Fixed: Dataloss when moving series folder to root folder with only different casing

Signed-off-by: Robin Dadswell <robin@dadswell.email>
pull/770/head
Taloth Saldono 4 years ago committed by Qstick
parent 7f64162a7a
commit da5cdd6661

@ -403,6 +403,58 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(source.FullName, destination.FullName); 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<IOException>(() => 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<IOException>(() => 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<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test] [Test]
public void CopyFolder_should_ignore_nfs_temp_file() public void CopyFolder_should_ignore_nfs_temp_file()
{ {
@ -452,6 +504,62 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyMoveFolder(original.FullName, source.FullName, destination.FullName); 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<IOException>(() => 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] [Test]
public void should_throw_if_destination_is_readonly() public void should_throw_if_destination_is_readonly()
{ {
@ -755,6 +863,10 @@ namespace NzbDrone.Common.Test.DiskTests
.Setup(v => v.CreateFolder(It.IsAny<string>())) .Setup(v => v.CreateFolder(It.IsAny<string>()))
.Callback<string>(v => fileSystem.Directory.CreateDirectory(v)); .Callback<string>(v => fileSystem.Directory.CreateDirectory(v));
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.MoveFolder(It.IsAny<string>(), It.IsAny<string>()))
.Callback<string, string>((v, r) => fileSystem.Directory.Move(v, r));
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(v => v.DeleteFolder(It.IsAny<string>(), It.IsAny<bool>())) .Setup(v => v.DeleteFolder(It.IsAny<string>(), It.IsAny<bool>()))
.Callback<string, bool>((v, r) => fileSystem.Directory.Delete(v, r)); .Callback<string, bool>((v, r) => fileSystem.Directory.Delete(v, r));

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading; using System.Threading;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Disk namespace NzbDrone.Common.Disk
@ -28,11 +27,53 @@ namespace NzbDrone.Common.Disk
_logger = logger; _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) public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
{ {
Ensure.That(sourcePath, () => sourcePath).IsValidPath(); Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).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)) if (mode == TransferMode.Move && !_diskProvider.FolderExists(targetPath))
{ {
var sourceMount = _diskProvider.GetMount(sourcePath); 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 we're on the same mount, do a simple folder move.
if (sourceMount != null && targetMount != null && sourceMount.RootDirectory == targetMount.RootDirectory) 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); _diskProvider.MoveFolder(sourcePath, targetPath);
return mode; return mode;
} }
@ -80,6 +121,13 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move)) 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); _diskProvider.DeleteFolder(sourcePath, true);
} }
@ -93,7 +141,10 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath(); Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).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)) if (!_diskProvider.FolderExists(targetPath))
{ {
@ -205,6 +256,9 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath(); Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath(); Ensure.That(targetPath, () => targetPath).IsValidPath();
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath); _logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath);
var originalSize = _diskProvider.GetFileSize(sourcePath); var originalSize = _diskProvider.GetFileSize(sourcePath);

Loading…
Cancel
Save