using System; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; using Mono.Unix; using Mono.Unix.Native; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; namespace NzbDrone.Mono.Disk { public class DiskProvider : DiskProviderBase { // Mono supports sending -1 for a uint to indicate that the owner or group should not be set // `unchecked((uint)-1)` and `uint.MaxValue` are the same thing. private const uint UNCHANGED_ID = uint.MaxValue; private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DiskProvider)); private readonly IProcMountProvider _procMountProvider; private readonly ISymbolicLinkResolver _symLinkResolver; public DiskProvider(IProcMountProvider procMountProvider, ISymbolicLinkResolver symLinkResolver) : this(new FileSystem(), procMountProvider, symLinkResolver) { } public DiskProvider(IFileSystem fileSystem, IProcMountProvider procMountProvider, ISymbolicLinkResolver symLinkResolver) : base(fileSystem) { _procMountProvider = procMountProvider; _symLinkResolver = symLinkResolver; } public override IMount GetMount(string path) { path = _symLinkResolver.GetCompleteRealPath(path); return base.GetMount(path); } public override long? GetAvailableSpace(string path) { Ensure.That(path, () => path).IsValidPath(); Logger.Debug($"path: {path}"); var mount = GetMount(path); if (mount == null) { Logger.Debug("Unable to get free space for '{0}', unable to find suitable drive", path); return null; } return mount.AvailableFreeSpace; } public override void InheritFolderPermissions(string filename) { Ensure.That(filename, () => filename).IsValidPath(); try { var fs = _fileSystem.File.GetAccessControl(filename); fs.SetAccessRuleProtection(false, false); _fileSystem.File.SetAccessControl(filename, fs); } catch (NotImplementedException) { } catch (PlatformNotSupportedException) { } } public override void SetPermissions(string path, string mask, string user, string group) { SetPermissions(path, mask); SetOwner(path, user, group); } public override void CopyPermissions(string sourcePath, string targetPath, bool includeOwner) { try { Syscall.stat(sourcePath, out var srcStat); Syscall.stat(targetPath, out var tgtStat); if (srcStat.st_mode != tgtStat.st_mode) { Syscall.chmod(targetPath, srcStat.st_mode); } if (includeOwner && (srcStat.st_uid != tgtStat.st_uid || srcStat.st_gid != tgtStat.st_gid)) { Syscall.chown(targetPath, srcStat.st_uid, srcStat.st_gid); } } catch (Exception ex) { Logger.Debug(ex, "Failed to copy permissions from {0} to {1}", sourcePath, targetPath); } } protected override List GetAllMounts() { return _procMountProvider.GetMounts() .Concat(GetDriveInfoMounts() .Select(d => new DriveInfoMount(d, FindDriveType.Find(d.DriveFormat))) .Where(d => d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)) .DistinctBy(v => v.RootDirectory) .ToList(); } protected override bool IsSpecialMount(IMount mount) { var root = mount.RootDirectory; if (root.StartsWith("/var/lib/")) { // Could be /var/lib/docker when docker uses zfs. Very unlikely that a useful mount is located in /var/lib. return true; } if (root.StartsWith("/snap/")) { // Mount point for snap packages return true; } return false; } public override long? GetTotalSize(string path) { Ensure.That(path, () => path).IsValidPath(); var mount = GetMount(path); return mount?.TotalSize; } protected override void CopyFileInternal(string source, string destination, bool overwrite) { var sourceInfo = UnixFileSystemInfo.GetFileSystemEntry(source); if (sourceInfo.IsSymbolicLink) { var isSameDir = UnixPath.GetDirectoryName(source) == UnixPath.GetDirectoryName(destination); var symlinkInfo = (UnixSymbolicLinkInfo)sourceInfo; var symlinkPath = symlinkInfo.ContentsPath; var newFile = new UnixSymbolicLinkInfo(destination); if (FileExists(destination) && overwrite) { DeleteFile(destination); } if (isSameDir) { // We're in the same dir, so we can preserve relative symlinks. newFile.CreateSymbolicLinkTo(symlinkInfo.ContentsPath); } else { var fullPath = UnixPath.Combine(UnixPath.GetDirectoryName(source), symlinkPath); newFile.CreateSymbolicLinkTo(fullPath); } } else { base.CopyFileInternal(source, destination, overwrite); } } protected override void MoveFileInternal(string source, string destination, bool overwrite) { var sourceInfo = UnixFileSystemInfo.GetFileSystemEntry(source); if (sourceInfo.IsSymbolicLink) { var isSameDir = UnixPath.GetDirectoryName(source) == UnixPath.GetDirectoryName(destination); var symlinkInfo = (UnixSymbolicLinkInfo)sourceInfo; var symlinkPath = symlinkInfo.ContentsPath; var newFile = new UnixSymbolicLinkInfo(destination); if (isSameDir) { // We're in the same dir, so we can preserve relative symlinks. newFile.CreateSymbolicLinkTo(symlinkInfo.ContentsPath); } else { var fullPath = UnixPath.Combine(UnixPath.GetDirectoryName(source), symlinkPath); newFile.CreateSymbolicLinkTo(fullPath); } try { // Finally remove the original symlink. symlinkInfo.Delete(); } catch { // Removing symlink failed, so rollback the new link and throw. newFile.Delete(); throw; } } else { base.MoveFileInternal(source, destination, overwrite); } } public override bool TryCreateHardLink(string source, string destination) { try { var fileInfo = UnixFileSystemInfo.GetFileSystemEntry(source); if (fileInfo.IsSymbolicLink) { return false; } fileInfo.CreateLink(destination); return true; } catch (Exception ex) { Logger.Debug(ex, string.Format("Hardlink '{0}' to '{1}' failed.", source, destination)); return false; } } private void SetPermissions(string path, string mask) { Logger.Debug("Setting permissions: {0} on {1}", mask, path); var filePermissions = NativeConvert.FromOctalPermissionString(mask); if (Syscall.chmod(path, filePermissions) < 0) { var error = Stdlib.GetLastError(); throw new LinuxPermissionsException("Error setting file permissions: " + error); } } private void SetOwner(string path, string user, string group) { if (string.IsNullOrWhiteSpace(user) && string.IsNullOrWhiteSpace(group)) { Logger.Debug("User and Group for chown not configured, skipping chown."); return; } var userId = GetUserId(user); var groupId = GetGroupId(group); if (Syscall.chown(path, userId, groupId) < 0) { var error = Stdlib.GetLastError(); throw new LinuxPermissionsException("Error setting file owner and/or group: " + error); } } private uint GetUserId(string user) { if (user.IsNullOrWhiteSpace()) { return UNCHANGED_ID; } uint userId; if (uint.TryParse(user, out userId)) { return userId; } var u = Syscall.getpwnam(user); if (u == null) { throw new LinuxPermissionsException("Unknown user: {0}", user); } return u.pw_uid; } private uint GetGroupId(string group) { if (group.IsNullOrWhiteSpace()) { return UNCHANGED_ID; } uint groupId; if (uint.TryParse(group, out groupId)) { return groupId; } var g = Syscall.getgrnam(group); if (g == null) { throw new LinuxPermissionsException("Unknown group: {0}", group); } return g.gr_gid; } } }