diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs index 7995a1dbb..7a9a89bff 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs @@ -403,6 +403,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() { @@ -452,6 +504,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() { @@ -755,6 +863,10 @@ namespace NzbDrone.Common.Test.DiskTests .Setup(v => v.CreateFolder(It.IsAny())) .Callback(v => fileSystem.Directory.CreateDirectory(v)); + Mocker.GetMock() + .Setup(v => v.MoveFolder(It.IsAny(), It.IsAny())) + .Callback((v, r) => fileSystem.Directory.Move(v, r)); + Mocker.GetMock() .Setup(v => v.DeleteFolder(It.IsAny(), It.IsAny())) .Callback((v, r) => fileSystem.Directory.Delete(v, r)); diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index 8a75ed34d..3a274ecfd 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading; using NLog; using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk @@ -28,11 +27,53 @@ namespace NzbDrone.Common.Disk _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 +82,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; } @@ -80,6 +121,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); } @@ -93,7 +141,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)) { @@ -205,6 +256,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);