From c54140169bbc10c68169fa86f5ec7f8a745b3369 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 27 Feb 2020 21:20:50 +0000 Subject: [PATCH] Fixed: Workaround for mono 6.x file copy/move issues --- src/NzbDrone.Mono/Disk/DiskProvider.cs | 148 +++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Mono/Disk/DiskProvider.cs b/src/NzbDrone.Mono/Disk/DiskProvider.cs index 5c0702a75..5958d73b9 100644 --- a/src/NzbDrone.Mono/Disk/DiskProvider.cs +++ b/src/NzbDrone.Mono/Disk/DiskProvider.cs @@ -8,8 +8,8 @@ using Mono.Unix.Native; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Instrumentation; namespace NzbDrone.Mono.Disk { @@ -19,24 +19,26 @@ namespace NzbDrone.Mono.Disk // `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 Logger _logger; private readonly IProcMountProvider _procMountProvider; private readonly ISymbolicLinkResolver _symLinkResolver; public DiskProvider(IProcMountProvider procMountProvider, - ISymbolicLinkResolver symLinkResolver) - : this(new FileSystem(), procMountProvider, symLinkResolver) + ISymbolicLinkResolver symLinkResolver, + Logger logger) + : this(new FileSystem(), procMountProvider, symLinkResolver, logger) { } public DiskProvider(IFileSystem fileSystem, IProcMountProvider procMountProvider, - ISymbolicLinkResolver symLinkResolver) + ISymbolicLinkResolver symLinkResolver, + Logger logger) : base(fileSystem) { _procMountProvider = procMountProvider; _symLinkResolver = symLinkResolver; + _logger = logger; } public override IMount GetMount(string path) @@ -50,13 +52,13 @@ namespace NzbDrone.Mono.Disk { Ensure.That(path, () => path).IsValidPath(); - Logger.Debug($"path: {path}"); + _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); + _logger.Debug("Unable to get free space for '{0}', unable to find suitable drive", path); return null; } @@ -106,7 +108,7 @@ namespace NzbDrone.Mono.Disk } catch (Exception ex) { - Logger.Debug(ex, "Failed to copy permissions from {0} to {1}", sourcePath, targetPath); + _logger.Debug(ex, "Failed to copy permissions from {0} to {1}", sourcePath, targetPath); } } @@ -178,6 +180,12 @@ namespace NzbDrone.Mono.Disk newFile.CreateSymbolicLinkTo(fullPath); } } + else if (((PlatformInfo.Platform == PlatformType.Mono && PlatformInfo.GetVersion() >= new Version(6, 0)) || + PlatformInfo.Platform == PlatformType.NetCore) && + (!FileExists(destination) || overwrite)) + { + TransferFilePatched(source, destination, overwrite, false); + } else { base.CopyFileInternal(source, destination, overwrite); @@ -219,12 +227,128 @@ namespace NzbDrone.Mono.Disk throw; } } + else if ((PlatformInfo.Platform == PlatformType.Mono && PlatformInfo.GetVersion() >= new Version(6, 0)) || + PlatformInfo.Platform == PlatformType.NetCore) + { + TransferFilePatched(source, destination, overwrite, true); + } else { base.MoveFileInternal(source, destination, overwrite); } } + private void TransferFilePatched(string source, string destination, bool overwrite, bool move) + { + // Mono 6.x throws errors if permissions or timestamps cannot be set + // - In 6.0 it'll leave a full length file + // - In 6.6 it'll leave a zero length file + // Catch the exception and attempt to handle these edgecases + try + { + if (move) + { + base.MoveFileInternal(source, destination, overwrite); + } + else + { + base.CopyFileInternal(source, destination, overwrite); + } + } + catch (UnauthorizedAccessException) + { + var srcInfo = new FileInfo(source); + var dstInfo = new FileInfo(destination); + var exists = dstInfo.Exists && srcInfo.Exists; + + if (PlatformInfo.Platform == PlatformType.Mono && PlatformInfo.GetVersion() >= new Version(6, 6) && + exists && dstInfo.Length == 0 && srcInfo.Length != 0) + { + // mono >=6.6 bug: zero length file since chmod happens at the start + _logger.Debug("{3} failed to {2} file likely due to known {3} bug, attempting to {2} directly. '{0}' -> '{1}'", source, destination, move ? "move" : "copy", PlatformInfo.PlatformName); + + try + { + _logger.Trace("Copying content from {0} to {1} ({2} bytes)", source, destination, srcInfo.Length); + using (var srcStream = new FileStream(source, FileMode.Open, FileAccess.Read)) + using (var dstStream = new FileStream(destination, FileMode.Create, FileAccess.Write)) + { + srcStream.CopyTo(dstStream); + } + } + catch + { + // If it fails again then bail + throw; + } + } + else if (((PlatformInfo.Platform == PlatformType.Mono && + PlatformInfo.GetVersion() >= new Version(6, 0) && + PlatformInfo.GetVersion() < new Version(6, 6)) || + PlatformInfo.Platform == PlatformType.NetCore) && + exists && dstInfo.Length == srcInfo.Length) + { + // mono 6.0, mono 6.4 and netcore 3.1 bug: full length file since utime and chmod happens at the end + _logger.Debug("{3} failed to {2} file likely due to known {3} bug, attempting to {2} directly. '{0}' -> '{1}'", source, destination, move ? "move" : "copy", PlatformInfo.PlatformName); + + // Check at least part of the file since UnauthorizedAccess can happen due to legitimate reasons too + var checkLength = (int)Math.Min(64 * 1024, dstInfo.Length); + if (checkLength > 0) + { + var srcData = new byte[checkLength]; + var dstData = new byte[checkLength]; + + _logger.Trace("Check last {0} bytes from {1}", checkLength, destination); + + using (var srcStream = new FileStream(source, FileMode.Open, FileAccess.Read)) + using (var dstStream = new FileStream(destination, FileMode.Open, FileAccess.Read)) + { + srcStream.Position = srcInfo.Length - checkLength; + dstStream.Position = dstInfo.Length - checkLength; + + srcStream.Read(srcData, 0, checkLength); + dstStream.Read(dstData, 0, checkLength); + } + + for (var i = 0; i < checkLength; i++) + { + if (srcData[i] != dstData[i]) + { + // Files aren't the same, the UnauthorizedAccess was unrelated + _logger.Trace("Copy was incomplete, rethrowing original error"); + throw; + } + } + + _logger.Trace("Copy was complete, finishing {0} operation", move ? "move" : "copy"); + } + } + else + { + // Unrecognized situation, the UnauthorizedAccess was unrelated + throw; + } + + if (exists) + { + try + { + dstInfo.LastWriteTimeUtc = srcInfo.LastWriteTimeUtc; + } + catch + { + _logger.Debug("Unable to change last modified date for {0}, skipping.", destination); + } + + if (move) + { + _logger.Trace("Removing source file {0}", source); + File.Delete(source); + } + } + } + } + public override bool TryCreateHardLink(string source, string destination) { try @@ -241,14 +365,14 @@ namespace NzbDrone.Mono.Disk } catch (Exception ex) { - Logger.Debug(ex, string.Format("Hardlink '{0}' to '{1}' failed.", source, destination)); + _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); + _logger.Debug("Setting permissions: {0} on {1}", mask, path); var filePermissions = NativeConvert.FromOctalPermissionString(mask); @@ -264,7 +388,7 @@ namespace NzbDrone.Mono.Disk { if (string.IsNullOrWhiteSpace(user) && string.IsNullOrWhiteSpace(group)) { - Logger.Debug("User and Group for chown not configured, skipping chown."); + _logger.Debug("User and Group for chown not configured, skipping chown."); return; }