1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-16 11:37:58 +02:00

Added support for Hardlinking instead of Copy.

This commit is contained in:
Taloth Saldono 2014-08-17 20:39:08 +02:00
parent a8bea777d7
commit ffa814f387
12 changed files with 217 additions and 57 deletions

View File

@ -19,5 +19,6 @@ public class MediaManagementConfigResource : RestResource
public String ChownGroup { get; set; }
public Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
public Boolean CopyUsingHardlinks { get; set; }
}
}

View File

@ -162,6 +162,72 @@ public void should_be_able_to_delete_directory_with_read_only_file()
Directory.Exists(sourceDir).Should().BeFalse();
}
[Test]
public void should_be_able_to_hardlink_file()
{
var sourceDir = GetTempFilePath();
var source = Path.Combine(sourceDir, "test.txt");
var destination = Path.Combine(sourceDir, "destination.txt");
Directory.CreateDirectory(sourceDir);
Subject.WriteAllText(source, "SourceFile");
var result = Subject.TransferFile(source, destination, TransferMode.HardLink);
result.Should().Be(TransferMode.HardLink);
File.AppendAllText(source, "Test");
File.ReadAllText(destination).Should().Be("SourceFileTest");
}
private void DoHardLinkRename(FileShare fileShare)
{
var sourceDir = GetTempFilePath();
var source = Path.Combine(sourceDir, "test.txt");
var destination = Path.Combine(sourceDir, "destination.txt");
var rename = Path.Combine(sourceDir, "rename.txt");
Directory.CreateDirectory(sourceDir);
Subject.WriteAllText(source, "SourceFile");
Subject.TransferFile(source, destination, TransferMode.HardLink);
using (var stream = new FileStream(source, FileMode.Open, FileAccess.Read, fileShare))
{
stream.ReadByte();
Subject.MoveFile(destination, rename);
stream.ReadByte();
}
File.Exists(rename).Should().BeTrue();
File.Exists(destination).Should().BeFalse();
File.AppendAllText(source, "Test");
File.ReadAllText(rename).Should().Be("SourceFileTest");
}
[Test]
public void should_be_able_to_rename_open_hardlinks_with_fileshare_delete()
{
DoHardLinkRename(FileShare.Delete);
}
[Test]
public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_none()
{
Assert.Throws<IOException>(() => DoHardLinkRename(FileShare.None));
}
[Test]
public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_write()
{
Assert.Throws<IOException>(() => DoHardLinkRename(FileShare.Read));
}
[Test]
public void empty_folder_should_return_folder_modified_date()
{

View File

@ -13,12 +13,6 @@ namespace NzbDrone.Common.Disk
{
public abstract class DiskProviderBase : IDiskProvider
{
enum TransferAction
{
Copy,
Move
}
private static readonly Logger Logger = NzbDroneLogger.GetLogger();
public abstract long? GetAvailableSpace(string path);
@ -152,7 +146,7 @@ public void CopyFolder(string source, string destination)
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
TransferFolder(source, destination, TransferAction.Copy);
TransferFolder(source, destination, TransferMode.Copy);
}
public void MoveFolder(string source, string destination)
@ -162,7 +156,7 @@ public void MoveFolder(string source, string destination)
try
{
TransferFolder(source, destination, TransferAction.Move);
TransferFolder(source, destination, TransferMode.Move);
DeleteFolder(source, true);
}
catch (Exception e)
@ -173,15 +167,15 @@ public void MoveFolder(string source, string destination)
}
}
private void TransferFolder(string source, string target, TransferAction transferAction)
public void TransferFolder(string source, string destination, TransferMode mode)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(target, () => target).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
Logger.ProgressDebug("{0} {1} -> {2}", transferAction, source, target);
Logger.ProgressDebug("{0} {1} -> {2}", mode, source, destination);
var sourceFolder = new DirectoryInfo(source);
var targetFolder = new DirectoryInfo(target);
var targetFolder = new DirectoryInfo(destination);
if (!targetFolder.Exists)
{
@ -190,28 +184,16 @@ private void TransferFolder(string source, string target, TransferAction transfe
foreach (var subDir in sourceFolder.GetDirectories())
{
TransferFolder(subDir.FullName, Path.Combine(target, subDir.Name), transferAction);
TransferFolder(subDir.FullName, Path.Combine(destination, subDir.Name), mode);
}
foreach (var sourceFile in sourceFolder.GetFiles("*.*", SearchOption.TopDirectoryOnly))
{
var destFile = Path.Combine(target, sourceFile.Name);
var destFile = Path.Combine(destination, sourceFile.Name);
Logger.ProgressDebug("{0} {1} -> {2}", transferAction, sourceFile, destFile);
Logger.ProgressDebug("{0} {1} -> {2}", mode, sourceFile, destFile);
switch (transferAction)
{
case TransferAction.Copy:
{
sourceFile.CopyTo(destFile, true);
break;
}
case TransferAction.Move:
{
MoveFile(sourceFile.FullName, destFile, true);
break;
}
}
TransferFile(sourceFile.FullName, destFile, mode, true);
}
}
@ -227,19 +209,15 @@ public void DeleteFile(string path)
public void CopyFile(string source, string destination, bool overwrite = false)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
if (source.PathEquals(destination))
{
Logger.Warn("Source and destination can't be the same {0}", source);
return;
}
File.Copy(source, destination, overwrite);
TransferFile(source, destination, TransferMode.Copy, overwrite);
}
public void MoveFile(string source, string destination, bool overwrite = false)
{
TransferFile(source, destination, TransferMode.Move, overwrite);
}
public TransferMode TransferFile(string source, string destination, TransferMode mode, bool overwrite)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
@ -247,7 +225,7 @@ public void MoveFile(string source, string destination, bool overwrite = false)
if (source.PathEquals(destination))
{
Logger.Warn("Source and destination can't be the same {0}", source);
return;
return TransferMode.None;
}
if (FileExists(destination) && overwrite)
@ -255,10 +233,37 @@ public void MoveFile(string source, string destination, bool overwrite = false)
DeleteFile(destination);
}
RemoveReadOnly(source);
File.Move(source, destination);
if (mode.HasFlag(TransferMode.HardLink))
{
bool createdHardlink = TryCreateHardLink(source, destination);
if (createdHardlink)
{
return TransferMode.HardLink;
}
else if (!mode.HasFlag(TransferMode.Copy))
{
throw new IOException("Hardlinking from '" + source + "' to '" + destination + "' failed.");
}
}
if (mode.HasFlag(TransferMode.Copy))
{
File.Copy(source, destination, overwrite);
return TransferMode.Copy;
}
if (mode.HasFlag(TransferMode.Move))
{
RemoveReadOnly(source);
File.Move(source, destination);
return TransferMode.Move;
}
return TransferMode.None;
}
public abstract bool TryCreateHardLink(string source, string destination);
public void DeleteFolder(string path, bool recursive)
{
Ensure.That(path, () => path).IsValidPath();

View File

@ -25,9 +25,12 @@ public interface IDiskProvider
void CreateFolder(string path);
void CopyFolder(string source, string destination);
void MoveFolder(string source, string destination);
void TransferFolder(string source, string destination, TransferMode transferMode);
void DeleteFile(string path);
void CopyFile(string source, string destination, bool overwrite = false);
void MoveFile(string source, string destination, bool overwrite = false);
TransferMode TransferFile(string source, string destination, TransferMode transferMode, bool overwrite = false);
bool TryCreateHardLink(string source, string destination);
void DeleteFolder(string path, bool recursive);
string ReadAllText(string filePath);
void WriteAllText(string filename, string contents);

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Common.Disk
{
[Flags]
public enum TransferMode
{
None = 0,
Move = 1,
Copy = 2,
HardLink = 4,
HardLinkOrCopy = Copy | HardLink
}
}

View File

@ -73,6 +73,7 @@
<Compile Include="DictionaryExtensions.cs" />
<Compile Include="Disk\DiskProviderBase.cs" />
<Compile Include="Disk\IDiskProvider.cs" />
<Compile Include="Disk\TransferMode.cs" />
<Compile Include="EnsureThat\Ensure.cs" />
<Compile Include="EnsureThat\EnsureBoolExtensions.cs" />
<Compile Include="EnsureThat\EnsureCollectionExtensions.cs" />

View File

@ -212,6 +212,13 @@ public Boolean SkipFreeSpaceCheckWhenImporting
set { SetValue("SkipFreeSpaceCheckWhenImporting", value); }
}
public Boolean CopyUsingHardlinks
{
get { return GetValueBoolean("CopyUsingHardlinks", true); }
set { SetValue("CopyUsingHardlinks", value); }
}
public Boolean SetPermissionsLinux
{
get { return GetValueBoolean("SetPermissionsLinux", false); }

View File

@ -36,6 +36,7 @@ public interface IConfigService
Boolean CreateEmptySeriesFolders { get; set; }
FileDateType FileDate { get; set; }
Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
Boolean CopyUsingHardlinks { get; set; }
//Permissions (Media Management)
Boolean SetPermissionsLinux { get; set; }

View File

@ -28,6 +28,7 @@ public class EpisodeFileMovingService : IMoveEpisodeFiles
private readonly IBuildFileNames _buildFileNames;
private readonly IDiskProvider _diskProvider;
private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IConfigService _configService;
private readonly Logger _logger;
public EpisodeFileMovingService(IEpisodeService episodeService,
@ -35,6 +36,7 @@ public EpisodeFileMovingService(IEpisodeService episodeService,
IBuildFileNames buildFileNames,
IDiskProvider diskProvider,
IMediaFileAttributeService mediaFileAttributeService,
IConfigService configService,
Logger logger)
{
_episodeService = episodeService;
@ -42,6 +44,7 @@ public EpisodeFileMovingService(IEpisodeService episodeService,
_buildFileNames = buildFileNames;
_diskProvider = diskProvider;
_mediaFileAttributeService = mediaFileAttributeService;
_configService = configService;
_logger = logger;
}
@ -53,7 +56,7 @@ public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series)
_logger.Debug("Renaming episode file: {0} to {1}", episodeFile, filePath);
return TransferFile(episodeFile, series, episodes, filePath, false);
return TransferFile(episodeFile, series, episodes, filePath, TransferMode.Move);
}
public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode)
@ -63,7 +66,7 @@ public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEp
_logger.Debug("Moving episode file: {0} to {1}", episodeFile, filePath);
return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, false);
return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, TransferMode.Move);
}
public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode)
@ -73,10 +76,17 @@ public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEp
_logger.Debug("Copying episode file: {0} to {1}", episodeFile, filePath);
return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, true);
if (_configService.CopyUsingHardlinks)
{
return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, TransferMode.HardLinkOrCopy);
}
else
{
return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, TransferMode.Copy);
}
}
private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List<Episode> episodes, String destinationFilename, Boolean copyOnly)
private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List<Episode> episodes, string destinationFilename, TransferMode mode)
{
Ensure.That(episodeFile, () => episodeFile).IsNotNull();
Ensure.That(series,() => series).IsNotNull();
@ -115,16 +125,8 @@ private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List<Ep
}
}
if (copyOnly)
{
_logger.Debug("Copying [{0}] > [{1}]", episodeFilePath, destinationFilename);
_diskProvider.CopyFile(episodeFilePath, destinationFilename);
}
else
{
_logger.Debug("Moving [{0}] > [{1}]", episodeFilePath, destinationFilename);
_diskProvider.MoveFile(episodeFilePath, destinationFilename);
}
_logger.Debug("{0} [{1}] > [{2}]", mode, episodeFilePath, destinationFilename);
_diskProvider.TransferFile(episodeFilePath, destinationFilename, mode);
episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilename);

View File

@ -7,6 +7,7 @@
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Instrumentation;
using Mono.Unix;
namespace NzbDrone.Mono
{
@ -151,5 +152,18 @@ private DriveInfo GetDriveInfo(string path)
.OrderByDescending(drive => drive.Name.Length)
.FirstOrDefault();
}
public override bool TryCreateHardLink(string source, string destination)
{
try
{
UnixFileSystemInfo.GetFileSystemEntry(source).CreateLink(destination);
return true;
}
catch
{
return false;
}
}
}
}

View File

@ -19,6 +19,10 @@ static extern bool GetDiskFreeSpaceEx(string lpDirectoryName,
out ulong lpTotalNumberOfBytes,
out ulong lpTotalNumberOfFreeBytes);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
public override long? GetAvailableSpace(string path)
{
Ensure.That(path, () => path).IsValidPath();
@ -98,5 +102,18 @@ private static long DriveTotalSizeEx(string folderName)
return 0;
}
public override bool TryCreateHardLink(string source, string destination)
{
try
{
return CreateHardLink(destination, source, IntPtr.Zero);
}
catch
{
return false;
}
}
}
}

View File

@ -25,10 +25,10 @@
</div>
</fieldset>
{{#if_mono}}
<fieldset class="advanced-setting">
<legend>Importing</legend>
{{#if_mono}}
<div class="form-group">
<label class="col-sm-3 control-label">Skip Free Space Check</label>
@ -51,5 +51,29 @@
</div>
</div>
</div>
</fieldset>
{{/if_mono}}
<div class="form-group">
<label class="col-sm-3 control-label">Use Hardlinks instead of Copy</label>
<div class="col-sm-9">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="copyUsingHardlinks"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-nd-form-info" title="Use Hardlinks when trying to copy files from seeding torrents"/>
<i class="icon-nd-form-warn" title="Occassionally, file locks may prevent renaming files that are currently seeding. Temporarily disable seeding while using the Rename UI to rename existing episodes to work around it."/>
</span>
</div>
</div>
</div>
</fieldset>