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:
parent
a8bea777d7
commit
ffa814f387
@ -19,5 +19,6 @@ public class MediaManagementConfigResource : RestResource
|
||||
public String ChownGroup { get; set; }
|
||||
|
||||
public Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
|
||||
public Boolean CopyUsingHardlinks { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
19
src/NzbDrone.Common/Disk/TransferMode.cs
Normal file
19
src/NzbDrone.Common/Disk/TransferMode.cs
Normal 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
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
@ -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); }
|
||||
|
@ -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; }
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user