diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index c5a79c02a..6054f5d34 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -200,6 +200,11 @@ namespace NzbDrone.Common.Disk throw new IOException(string.Format("Source and destination can't be the same {0}", source)); } + CopyFileInternal(source, destination, overwrite); + } + + protected virtual void CopyFileInternal(string source, string destination, bool overwrite = false) + { File.Copy(source, destination, overwrite); } @@ -219,6 +224,11 @@ namespace NzbDrone.Common.Disk } RemoveReadOnly(source); + MoveFileInternal(source, destination); + } + + protected virtual void MoveFileInternal(string source, string destination) + { File.Move(source, destination); } diff --git a/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs b/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs index 47cb7fccc..7a768e702 100644 --- a/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs +++ b/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs @@ -1,4 +1,6 @@ -using System; +using System; +using System.IO; +using FluentAssertions; using Mono.Unix; using NUnit.Framework; using NzbDrone.Common.Test.DiskTests; @@ -35,5 +37,55 @@ namespace NzbDrone.Mono.Test.DiskProviderTests entry.FileAccessPermissions &= ~(FileAccessPermissions.UserWrite | FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite); } } + + [Test] + public void should_move_symlink() + { + var tempFolder = GetTempFilePath(); + Directory.CreateDirectory(tempFolder); + + var file = Path.Combine(tempFolder, "target.txt"); + var source = Path.Combine(tempFolder, "symlink_source.txt"); + var destination = Path.Combine(tempFolder, "symlink_destination.txt"); + + File.WriteAllText(file, "Some content"); + + new UnixSymbolicLinkInfo(source).CreateSymbolicLinkTo(file); + + Subject.MoveFile(source, destination); + + File.Exists(file).Should().BeTrue(); + File.Exists(source).Should().BeFalse(); + File.Exists(destination).Should().BeTrue(); + UnixFileSystemInfo.GetFileSystemEntry(destination).IsSymbolicLink.Should().BeTrue(); + + File.ReadAllText(destination).Should().Be("Some content"); + } + + [Test] + public void should_copy_symlink() + { + var tempFolder = GetTempFilePath(); + Directory.CreateDirectory(tempFolder); + + var file = Path.Combine(tempFolder, "target.txt"); + var source = Path.Combine(tempFolder, "symlink_source.txt"); + var destination = Path.Combine(tempFolder, "symlink_destination.txt"); + + File.WriteAllText(file, "Some content"); + + new UnixSymbolicLinkInfo(source).CreateSymbolicLinkTo(file); + + Subject.CopyFile(source, destination); + + File.Exists(file).Should().BeTrue(); + File.Exists(source).Should().BeTrue(); + File.Exists(destination).Should().BeTrue(); + UnixFileSystemInfo.GetFileSystemEntry(source).IsSymbolicLink.Should().BeTrue(); + UnixFileSystemInfo.GetFileSystemEntry(destination).IsSymbolicLink.Should().BeTrue(); + + File.ReadAllText(source).Should().Be("Some content"); + File.ReadAllText(destination).Should().Be("Some content"); + } } } diff --git a/src/NzbDrone.Mono/Disk/DiskProvider.cs b/src/NzbDrone.Mono/Disk/DiskProvider.cs index a2f52139c..759f8d3e3 100644 --- a/src/NzbDrone.Mono/Disk/DiskProvider.cs +++ b/src/NzbDrone.Mono/Disk/DiskProvider.cs @@ -96,11 +96,90 @@ namespace NzbDrone.Mono.Disk 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) + { + 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); + } + } + public override bool TryCreateHardLink(string source, string destination) { try { - UnixFileSystemInfo.GetFileSystemEntry(source).CreateLink(destination); + var fileInfo = UnixFileSystemInfo.GetFileSystemEntry(source); + + if (fileInfo.IsSymbolicLink) return false; + + fileInfo.CreateLink(destination); return true; } catch (Exception ex)