1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-09-19 16:01:46 +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 String ChownGroup { get; set; }
public Boolean SkipFreeSpaceCheckWhenImporting { 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(); 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] [Test]
public void empty_folder_should_return_folder_modified_date() public void empty_folder_should_return_folder_modified_date()
{ {

View File

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

View File

@ -25,9 +25,12 @@ public interface IDiskProvider
void CreateFolder(string path); void CreateFolder(string path);
void CopyFolder(string source, string destination); void CopyFolder(string source, string destination);
void MoveFolder(string source, string destination); void MoveFolder(string source, string destination);
void TransferFolder(string source, string destination, TransferMode transferMode);
void DeleteFile(string path); void DeleteFile(string path);
void CopyFile(string source, string destination, bool overwrite = false); void CopyFile(string source, string destination, bool overwrite = false);
void MoveFile(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); void DeleteFolder(string path, bool recursive);
string ReadAllText(string filePath); string ReadAllText(string filePath);
void WriteAllText(string filename, string contents); 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="DictionaryExtensions.cs" />
<Compile Include="Disk\DiskProviderBase.cs" /> <Compile Include="Disk\DiskProviderBase.cs" />
<Compile Include="Disk\IDiskProvider.cs" /> <Compile Include="Disk\IDiskProvider.cs" />
<Compile Include="Disk\TransferMode.cs" />
<Compile Include="EnsureThat\Ensure.cs" /> <Compile Include="EnsureThat\Ensure.cs" />
<Compile Include="EnsureThat\EnsureBoolExtensions.cs" /> <Compile Include="EnsureThat\EnsureBoolExtensions.cs" />
<Compile Include="EnsureThat\EnsureCollectionExtensions.cs" /> <Compile Include="EnsureThat\EnsureCollectionExtensions.cs" />

View File

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

View File

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

View File

@ -28,6 +28,7 @@ public class EpisodeFileMovingService : IMoveEpisodeFiles
private readonly IBuildFileNames _buildFileNames; private readonly IBuildFileNames _buildFileNames;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IConfigService _configService;
private readonly Logger _logger; private readonly Logger _logger;
public EpisodeFileMovingService(IEpisodeService episodeService, public EpisodeFileMovingService(IEpisodeService episodeService,
@ -35,6 +36,7 @@ public EpisodeFileMovingService(IEpisodeService episodeService,
IBuildFileNames buildFileNames, IBuildFileNames buildFileNames,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IMediaFileAttributeService mediaFileAttributeService, IMediaFileAttributeService mediaFileAttributeService,
IConfigService configService,
Logger logger) Logger logger)
{ {
_episodeService = episodeService; _episodeService = episodeService;
@ -42,6 +44,7 @@ public EpisodeFileMovingService(IEpisodeService episodeService,
_buildFileNames = buildFileNames; _buildFileNames = buildFileNames;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_mediaFileAttributeService = mediaFileAttributeService; _mediaFileAttributeService = mediaFileAttributeService;
_configService = configService;
_logger = logger; _logger = logger;
} }
@ -53,7 +56,7 @@ public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series)
_logger.Debug("Renaming episode file: {0} to {1}", episodeFile, filePath); _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) 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); _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) 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); _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(episodeFile, () => episodeFile).IsNotNull();
Ensure.That(series,() => series).IsNotNull(); Ensure.That(series,() => series).IsNotNull();
@ -115,16 +125,8 @@ private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List<Ep
} }
} }
if (copyOnly) _logger.Debug("{0} [{1}] > [{2}]", mode, episodeFilePath, destinationFilename);
{ _diskProvider.TransferFile(episodeFilePath, destinationFilename, mode);
_logger.Debug("Copying [{0}] > [{1}]", episodeFilePath, destinationFilename);
_diskProvider.CopyFile(episodeFilePath, destinationFilename);
}
else
{
_logger.Debug("Moving [{0}] > [{1}]", episodeFilePath, destinationFilename);
_diskProvider.MoveFile(episodeFilePath, destinationFilename);
}
episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilename); episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilename);

View File

@ -7,6 +7,7 @@
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using Mono.Unix;
namespace NzbDrone.Mono namespace NzbDrone.Mono
{ {
@ -151,5 +152,18 @@ private DriveInfo GetDriveInfo(string path)
.OrderByDescending(drive => drive.Name.Length) .OrderByDescending(drive => drive.Name.Length)
.FirstOrDefault(); .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 lpTotalNumberOfBytes,
out ulong lpTotalNumberOfFreeBytes); 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) public override long? GetAvailableSpace(string path)
{ {
Ensure.That(path, () => path).IsValidPath(); Ensure.That(path, () => path).IsValidPath();
@ -98,5 +102,18 @@ private static long DriveTotalSizeEx(string folderName)
return 0; 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> </div>
</fieldset> </fieldset>
{{#if_mono}}
<fieldset class="advanced-setting"> <fieldset class="advanced-setting">
<legend>Importing</legend> <legend>Importing</legend>
{{#if_mono}}
<div class="form-group"> <div class="form-group">
<label class="col-sm-3 control-label">Skip Free Space Check</label> <label class="col-sm-3 control-label">Skip Free Space Check</label>
@ -51,5 +51,29 @@
</div> </div>
</div> </div>
</div> </div>
</fieldset>
{{/if_mono}} {{/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>