From 94a78eabe555389bd967365e37868725f12c3f31 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 24 Sep 2020 11:53:20 +0200 Subject: [PATCH] Fixed: Dataloss when moving series folder to root folder with only different casing --- .../DiskTests/DiskTransferServiceFixture.cs | 114 +++++++++++++++++- .../Disk/DiskTransferService.cs | 61 +++++++++- 2 files changed, 170 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs index c3833c4cb..16848b3b7 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs @@ -402,6 +402,58 @@ namespace NzbDrone.Common.Test.DiskTests VerifyCopyFolder(source.FullName, destination.FullName); } + [Test] + public void CopyFolder_should_detect_caseinsensitive_parents() + { + WindowsOnly(); + + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var root = new DirectoryInfo(GetTempFilePath()); + var source = new DirectoryInfo(root.FullName + "A/series"); + var destination = new DirectoryInfo(root.FullName + "a/series"); + + Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); + + Assert.Throws(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy)); + } + + [Test] + public void CopyFolder_should_detect_caseinsensitive_folder() + { + WindowsOnly(); + + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var root = new DirectoryInfo(GetTempFilePath()); + var source = new DirectoryInfo(root.FullName + "A/series"); + var destination = new DirectoryInfo(root.FullName + "A/Series"); + + Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); + + Assert.Throws(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy)); + } + + [Test] + public void CopyFolder_should_not_copy_casesensitive_folder() + { + MonoOnly(); + + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var root = new DirectoryInfo(GetTempFilePath()); + var source = new DirectoryInfo(root.FullName + "A/series"); + var destination = new DirectoryInfo(root.FullName + "A/Series"); + + Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); + + // Note: Although technically possible top copy to different case, we're not allowing it + Assert.Throws(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy)); + } + [Test] public void CopyFolder_should_ignore_nfs_temp_file() { @@ -451,6 +503,62 @@ namespace NzbDrone.Common.Test.DiskTests VerifyMoveFolder(original.FullName, source.FullName, destination.FullName); } + [Test] + public void MoveFolder_should_detect_caseinsensitive_parents() + { + WindowsOnly(); + + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var root = new DirectoryInfo(GetTempFilePath()); + var source = new DirectoryInfo(root.FullName + "A/series"); + var destination = new DirectoryInfo(root.FullName + "a/series"); + + Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); + + Assert.Throws(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move)); + } + + [Test] + public void MoveFolder_should_rename_caseinsensitive_folder() + { + WindowsOnly(); + + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var root = new DirectoryInfo(GetTempFilePath()); + var source = new DirectoryInfo(root.FullName + "A/series"); + var destination = new DirectoryInfo(root.FullName + "A/Series"); + + Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); + + Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move); + + source.FullName.GetActualCasing().Should().Be(destination.FullName); + } + + [Test] + public void MoveFolder_should_rename_casesensitive_folder() + { + MonoOnly(); + + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var root = new DirectoryInfo(GetTempFilePath()); + var source = new DirectoryInfo(root.FullName + "A/series"); + var destination = new DirectoryInfo(root.FullName + "A/Series"); + + Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); + + Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move); + + Directory.Exists(source.FullName).Should().Be(false); + Directory.Exists(destination.FullName).Should().Be(true); + } + [Test] public void should_throw_if_destination_is_readonly() { @@ -747,9 +855,13 @@ namespace NzbDrone.Common.Test.DiskTests .Setup(v => v.CreateFolder(It.IsAny())) .Callback(v => Directory.CreateDirectory(v)); + Mocker.GetMock() + .Setup(v => v.MoveFolder(It.IsAny(), It.IsAny())) + .Callback((v, r) => Directory.Move(v, r)); + Mocker.GetMock() .Setup(v => v.DeleteFolder(It.IsAny(), It.IsAny())) - .Callback((v,r) => Directory.Delete(v, r)); + .Callback((v, r) => Directory.Delete(v, r)); Mocker.GetMock() .Setup(v => v.DeleteFile(It.IsAny())) diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index b2b1733cf..4ad4b6b56 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -4,8 +4,6 @@ using System.Linq; using System.Threading; using NLog; using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Exceptions; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk @@ -27,12 +25,54 @@ namespace NzbDrone.Common.Disk _diskProvider = diskProvider; _logger = logger; } + + private string ResolveRealParentPath(string path) + { + var parentPath = path.GetParentPath(); + if (!_diskProvider.FolderExists(path)) + { + return path; + } + + parentPath = parentPath.GetActualCasing(); + return parentPath + Path.DirectorySeparatorChar + Path.GetFileName(path); + } public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode) { Ensure.That(sourcePath, () => sourcePath).IsValidPath(); Ensure.That(targetPath, () => targetPath).IsValidPath(); + sourcePath = ResolveRealParentPath(sourcePath); + targetPath = ResolveRealParentPath(targetPath); + + _logger.Debug("{0} Directory [{1}] > [{2}]", mode, sourcePath, targetPath); + + if (sourcePath == targetPath) + { + throw new IOException(string.Format("Source and destination can't be the same {0}", sourcePath)); + } + + if (mode == TransferMode.Move && sourcePath.PathEquals(targetPath, StringComparison.InvariantCultureIgnoreCase) && _diskProvider.FolderExists(targetPath)) + { + // Move folder out of the way to allow case-insensitive renames + var tempPath = sourcePath + ".backup~"; + _logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", sourcePath, tempPath); + _diskProvider.MoveFolder(sourcePath, tempPath); + + if (!_diskProvider.FolderExists(targetPath)) + { + _logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, targetPath); + _logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath); + _diskProvider.MoveFolder(tempPath, targetPath); + return mode; + } + + // There were two separate folders, revert the intermediate rename and let the recursion deal with it + _logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, sourcePath); + _diskProvider.MoveFolder(tempPath, sourcePath); + } + if (mode == TransferMode.Move && !_diskProvider.FolderExists(targetPath)) { var sourceMount = _diskProvider.GetMount(sourcePath); @@ -41,7 +81,7 @@ namespace NzbDrone.Common.Disk // If we're on the same mount, do a simple folder move. if (sourceMount != null && targetMount != null && sourceMount.RootDirectory == targetMount.RootDirectory) { - _logger.Debug("Move Directory [{0}] > [{1}]", sourcePath, targetPath); + _logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath); _diskProvider.MoveFolder(sourcePath, targetPath); return mode; } @@ -74,6 +114,13 @@ namespace NzbDrone.Common.Disk if (mode.HasFlag(TransferMode.Move)) { + var totalSize = _diskProvider.GetFileInfos(sourcePath).Sum(v => v.Length); + + if (totalSize > (100 * 1024L * 1024L)) + { + throw new IOException($"Large files still exist in {sourcePath} after folder move, not deleting source folder"); + } + _diskProvider.DeleteFolder(sourcePath, true); } @@ -87,7 +134,10 @@ namespace NzbDrone.Common.Disk Ensure.That(sourcePath, () => sourcePath).IsValidPath(); Ensure.That(targetPath, () => targetPath).IsValidPath(); - _logger.Debug("Mirror [{0}] > [{1}]", sourcePath, targetPath); + sourcePath = ResolveRealParentPath(sourcePath); + targetPath = ResolveRealParentPath(targetPath); + + _logger.Debug("Mirror Folder [{0}] > [{1}]", sourcePath, targetPath); if (!_diskProvider.FolderExists(targetPath)) { @@ -187,6 +237,9 @@ namespace NzbDrone.Common.Disk Ensure.That(sourcePath, () => sourcePath).IsValidPath(); Ensure.That(targetPath, () => targetPath).IsValidPath(); + sourcePath = ResolveRealParentPath(sourcePath); + targetPath = ResolveRealParentPath(targetPath); + _logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath); var originalSize = _diskProvider.GetFileSize(sourcePath);