mirror of
https://github.com/Readarr/Readarr
synced 2025-12-26 18:22:32 +01:00
New: Fast copy using reflink on btrfs volumes
Signed-off-by: Robin Dadswell <robin@dadswell.email>
This commit is contained in:
parent
4af4d45873
commit
7002628514
7 changed files with 358 additions and 4 deletions
|
|
@ -212,6 +212,24 @@ public void DeleteFile(string path)
|
|||
_fileSystem.File.Delete(path);
|
||||
}
|
||||
|
||||
public void CloneFile(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
Ensure.That(destination, () => destination).IsValidPath();
|
||||
|
||||
if (source.PathEquals(destination))
|
||||
{
|
||||
throw new IOException(string.Format("Source and destination can't be the same {0}", source));
|
||||
}
|
||||
|
||||
CloneFileInternal(source, destination, overwrite);
|
||||
}
|
||||
|
||||
protected virtual void CloneFileInternal(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
CopyFileInternal(source, destination, overwrite);
|
||||
}
|
||||
|
||||
public void CopyFile(string source, string destination, bool overwrite = false)
|
||||
{
|
||||
Ensure.That(source, () => source).IsValidPath();
|
||||
|
|
@ -262,8 +280,18 @@ protected virtual void MoveFileInternal(string source, string destination)
|
|||
_fileSystem.File.Move(source, destination);
|
||||
}
|
||||
|
||||
public virtual bool TryRenameFile(string source, string destination)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract bool TryCreateHardLink(string source, string destination);
|
||||
|
||||
public virtual bool TryCreateRefLink(string source, string destination)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void DeleteFolder(string path, bool recursive)
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath();
|
||||
|
|
|
|||
|
|
@ -284,18 +284,45 @@ public TransferMode TransferFile(string sourcePath, string targetPath, TransferM
|
|||
var targetDriveFormat = targetMount?.DriveFormat ?? string.Empty;
|
||||
|
||||
var isCifs = targetDriveFormat == "cifs";
|
||||
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
||||
|
||||
if (mode.HasFlag(TransferMode.Copy))
|
||||
{
|
||||
if (isBtrfs)
|
||||
{
|
||||
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
||||
{
|
||||
return TransferMode.Copy;
|
||||
}
|
||||
}
|
||||
|
||||
TryCopyFileVerified(sourcePath, targetPath, originalSize);
|
||||
return TransferMode.Copy;
|
||||
}
|
||||
|
||||
if (mode.HasFlag(TransferMode.Move))
|
||||
{
|
||||
if (isBtrfs)
|
||||
{
|
||||
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
||||
{
|
||||
_logger.Trace("Renamed [{0}] to [{1}].", sourcePath, targetPath);
|
||||
return TransferMode.Move;
|
||||
}
|
||||
|
||||
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
||||
{
|
||||
_logger.Trace("Reflink successful, deleting source [{0}].", sourcePath);
|
||||
_diskProvider.DeleteFile(sourcePath);
|
||||
return TransferMode.Move;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCifs && !isSameMount)
|
||||
{
|
||||
_logger.Trace("On cifs mount. Starting verified copy [{0}] to [{1}].", sourcePath, targetPath);
|
||||
TryCopyFileVerified(sourcePath, targetPath, originalSize);
|
||||
_logger.Trace("Copy successful, deleting source [{0}].", sourcePath);
|
||||
_diskProvider.DeleteFile(sourcePath);
|
||||
return TransferMode.Move;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,13 @@ public interface IDiskProvider
|
|||
long GetFileSize(string path);
|
||||
void CreateFolder(string path);
|
||||
void DeleteFile(string path);
|
||||
void CloneFile(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 MoveFolder(string source, string destination);
|
||||
bool TryRenameFile(string source, string destination);
|
||||
bool TryCreateHardLink(string source, string destination);
|
||||
bool TryCreateRefLink(string source, string destination);
|
||||
void DeleteFolder(string path, bool recursive);
|
||||
string ReadAllText(string filePath);
|
||||
void WriteAllText(string filename, string contents);
|
||||
|
|
|
|||
|
|
@ -23,20 +23,27 @@ public class DiskProvider : DiskProviderBase
|
|||
|
||||
private readonly IProcMountProvider _procMountProvider;
|
||||
private readonly ISymbolicLinkResolver _symLinkResolver;
|
||||
private readonly IRefLinkCreator _createRefLink;
|
||||
|
||||
public DiskProvider(IProcMountProvider procMountProvider,
|
||||
ISymbolicLinkResolver symLinkResolver)
|
||||
: this(new FileSystem(), procMountProvider, symLinkResolver)
|
||||
ISymbolicLinkResolver symLinkResolver,
|
||||
IRefLinkCreator createRefLink,
|
||||
Logger logger)
|
||||
: this(new FileSystem(), procMountProvider, symLinkResolver, createRefLink, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public DiskProvider(IFileSystem fileSystem,
|
||||
IProcMountProvider procMountProvider,
|
||||
ISymbolicLinkResolver symLinkResolver)
|
||||
ISymbolicLinkResolver symLinkResolver,
|
||||
IRefLinkCreator createRefLink,
|
||||
Logger logger)
|
||||
: base(fileSystem)
|
||||
{
|
||||
_procMountProvider = procMountProvider;
|
||||
_symLinkResolver = symLinkResolver;
|
||||
_createRefLink = createRefLink;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override IMount GetMount(string path)
|
||||
|
|
@ -77,7 +84,7 @@ public override void SetPermissions(string path, string mask)
|
|||
|
||||
var permissions = NativeConvert.FromOctalPermissionString(mask);
|
||||
|
||||
if (Directory.Exists(path))
|
||||
if (_fileSystem.Directory.Exists(path))
|
||||
{
|
||||
permissions = GetFolderPermissions(permissions);
|
||||
}
|
||||
|
|
@ -184,6 +191,19 @@ protected override bool IsSpecialMount(IMount mount)
|
|||
return mount?.TotalSize;
|
||||
}
|
||||
|
||||
protected override void CloneFileInternal(string source, string destination, bool overwrite)
|
||||
{
|
||||
if (!FileExists(destination) && !UnixFileSystemInfo.GetFileSystemEntry(source).IsSymbolicLink)
|
||||
{
|
||||
if (_createRefLink.TryCreateRefLink(source, destination))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CopyFileInternal(source, destination, overwrite);
|
||||
}
|
||||
|
||||
protected override void CopyFileInternal(string source, string destination, bool overwrite)
|
||||
{
|
||||
var sourceInfo = UnixFileSystemInfo.GetFileSystemEntry(source);
|
||||
|
|
@ -259,6 +279,137 @@ protected override void MoveFileInternal(string source, string destination)
|
|||
}
|
||||
}
|
||||
|
||||
private void TransferFilePatched(string source, string destination, bool overwrite, bool move)
|
||||
{
|
||||
// Mono 6.x throws errors if permissions or timestamps cannot be set
|
||||
// - In 6.0 it'll leave a full length file
|
||||
// - In 6.6 it'll leave a zero length file
|
||||
// Catch the exception and attempt to handle these edgecases
|
||||
|
||||
// Mono 6.x till 6.10 doesn't properly try use rename first.
|
||||
if (move &&
|
||||
((PlatformInfo.Platform == PlatformType.Mono && PlatformInfo.GetVersion() < new Version(6, 10)) ||
|
||||
(PlatformInfo.Platform == PlatformType.NetCore)))
|
||||
{
|
||||
if (Syscall.lstat(source, out var sourcestat) == 0 &&
|
||||
Syscall.lstat(destination, out var deststat) != 0 &&
|
||||
Syscall.rename(source, destination) == 0)
|
||||
{
|
||||
_logger.Trace("Moved '{0}' -> '{1}' using Syscall.rename", source, destination);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (move)
|
||||
{
|
||||
base.MoveFileInternal(source, destination);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.CopyFileInternal(source, destination);
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
var srcInfo = new FileInfo(source);
|
||||
var dstInfo = new FileInfo(destination);
|
||||
var exists = dstInfo.Exists && srcInfo.Exists;
|
||||
|
||||
if (PlatformInfo.Platform == PlatformType.Mono && PlatformInfo.GetVersion() >= new Version(6, 6) &&
|
||||
exists && dstInfo.Length == 0 && srcInfo.Length != 0)
|
||||
{
|
||||
// mono >=6.6 bug: zero length file since chmod happens at the start
|
||||
_logger.Debug("{3} failed to {2} file likely due to known {3} bug, attempting to {2} directly. '{0}' -> '{1}'", source, destination, move ? "move" : "copy", PlatformInfo.PlatformName);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Trace("Copying content from {0} to {1} ({2} bytes)", source, destination, srcInfo.Length);
|
||||
using (var srcStream = new FileStream(source, FileMode.Open, FileAccess.Read))
|
||||
using (var dstStream = new FileStream(destination, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
srcStream.CopyTo(dstStream);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If it fails again then bail
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else if (((PlatformInfo.Platform == PlatformType.Mono &&
|
||||
PlatformInfo.GetVersion() >= new Version(6, 0) &&
|
||||
PlatformInfo.GetVersion() < new Version(6, 6)) ||
|
||||
PlatformInfo.Platform == PlatformType.NetCore) &&
|
||||
exists && dstInfo.Length == srcInfo.Length)
|
||||
{
|
||||
// mono 6.0, mono 6.4 and netcore 3.1 bug: full length file since utime and chmod happens at the end
|
||||
_logger.Debug("{3} failed to {2} file likely due to known {3} bug, attempting to {2} directly. '{0}' -> '{1}'", source, destination, move ? "move" : "copy", PlatformInfo.PlatformName);
|
||||
|
||||
// Check at least part of the file since UnauthorizedAccess can happen due to legitimate reasons too
|
||||
var checkLength = (int)Math.Min(64 * 1024, dstInfo.Length);
|
||||
if (checkLength > 0)
|
||||
{
|
||||
var srcData = new byte[checkLength];
|
||||
var dstData = new byte[checkLength];
|
||||
|
||||
_logger.Trace("Check last {0} bytes from {1}", checkLength, destination);
|
||||
|
||||
using (var srcStream = new FileStream(source, FileMode.Open, FileAccess.Read))
|
||||
using (var dstStream = new FileStream(destination, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
srcStream.Position = srcInfo.Length - checkLength;
|
||||
dstStream.Position = dstInfo.Length - checkLength;
|
||||
|
||||
srcStream.Read(srcData, 0, checkLength);
|
||||
dstStream.Read(dstData, 0, checkLength);
|
||||
}
|
||||
|
||||
for (var i = 0; i < checkLength; i++)
|
||||
{
|
||||
if (srcData[i] != dstData[i])
|
||||
{
|
||||
// Files aren't the same, the UnauthorizedAccess was unrelated
|
||||
_logger.Trace("Copy was incomplete, rethrowing original error");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Trace("Copy was complete, finishing {0} operation", move ? "move" : "copy");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unrecognized situation, the UnauthorizedAccess was unrelated
|
||||
throw;
|
||||
}
|
||||
|
||||
if (exists)
|
||||
{
|
||||
try
|
||||
{
|
||||
dstInfo.LastWriteTimeUtc = srcInfo.LastWriteTimeUtc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.Debug("Unable to change last modified date for {0}, skipping.", destination);
|
||||
}
|
||||
|
||||
if (move)
|
||||
{
|
||||
_logger.Trace("Removing source file {0}", source);
|
||||
File.Delete(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool TryRenameFile(string source, string destination)
|
||||
{
|
||||
return Syscall.rename(source, destination) == 0;
|
||||
}
|
||||
|
||||
public override bool TryCreateHardLink(string source, string destination)
|
||||
{
|
||||
try
|
||||
|
|
@ -280,6 +431,11 @@ public override bool TryCreateHardLink(string source, string destination)
|
|||
}
|
||||
}
|
||||
|
||||
public override bool TryCreateRefLink(string source, string destination)
|
||||
{
|
||||
return _createRefLink.TryCreateRefLink(source, destination);
|
||||
}
|
||||
|
||||
private uint GetUserId(string user)
|
||||
{
|
||||
if (user.IsNullOrWhiteSpace())
|
||||
|
|
|
|||
75
src/NzbDrone.Mono/Disk/RefLinkCreator.cs
Normal file
75
src/NzbDrone.Mono/Disk/RefLinkCreator.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using Mono.Unix;
|
||||
using Mono.Unix.Native;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Mono.Interop;
|
||||
|
||||
namespace NzbDrone.Mono.Disk
|
||||
{
|
||||
public interface IRefLinkCreator
|
||||
{
|
||||
bool TryCreateRefLink(string srcPath, string linkPath);
|
||||
}
|
||||
|
||||
public class RefLinkCreator : IRefLinkCreator
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly bool _supported;
|
||||
|
||||
public RefLinkCreator(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// Only support x86_64 because we know the FICLONE value is valid for it
|
||||
_supported = OsInfo.IsLinux && (Syscall.uname(out var results) == 0 && results.machine == "x86_64");
|
||||
}
|
||||
|
||||
public bool TryCreateRefLink(string srcPath, string linkPath)
|
||||
{
|
||||
if (!_supported)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var srcHandle = NativeMethods.open(srcPath, OpenFlags.O_RDONLY))
|
||||
{
|
||||
if (srcHandle.IsInvalid)
|
||||
{
|
||||
_logger.Trace("Failed to create reflink at '{0}' to '{1}': Couldn't open source file", linkPath, srcPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
using (var linkHandle = NativeMethods.open(linkPath, OpenFlags.O_WRONLY | OpenFlags.O_CREAT | OpenFlags.O_TRUNC))
|
||||
{
|
||||
if (linkHandle.IsInvalid)
|
||||
{
|
||||
_logger.Trace("Failed to create reflink at '{0}' to '{1}': Couldn't create new link file", linkPath, srcPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NativeMethods.clone_file(linkHandle, srcHandle) == -1)
|
||||
{
|
||||
var error = new UnixIOException();
|
||||
linkHandle.Dispose();
|
||||
Syscall.unlink(linkPath);
|
||||
_logger.Trace("Failed to create reflink at '{0}' to '{1}': {2}", linkPath, srcPath, error.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Trace("Created reflink at '{0}' to '{1}'", linkPath, srcPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Syscall.unlink(linkPath);
|
||||
_logger.Trace(ex, "Failed to create reflink at '{0}' to '{1}'", linkPath, srcPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/NzbDrone.Mono/Interop/NativeMethods.cs
Normal file
28
src/NzbDrone.Mono/Interop/NativeMethods.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Mono.Unix.Native;
|
||||
|
||||
namespace NzbDrone.Mono.Interop
|
||||
{
|
||||
internal enum IoctlRequest : uint
|
||||
{
|
||||
// Hardcoded ioctl for FICLONE on a typical linux system
|
||||
// #define FICLONE _IOW(0x94, 9, int)
|
||||
FICLONE = 0x40049409
|
||||
}
|
||||
|
||||
internal static class NativeMethods
|
||||
{
|
||||
[DllImport("libc", EntryPoint = "ioctl", SetLastError = true)]
|
||||
private static extern int Ioctl(SafeUnixHandle dst_fd, IoctlRequest request, SafeUnixHandle src_fd);
|
||||
|
||||
public static SafeUnixHandle open(string pathname, OpenFlags flags)
|
||||
{
|
||||
return new SafeUnixHandle(Syscall.open(pathname, flags));
|
||||
}
|
||||
|
||||
internal static int clone_file(SafeUnixHandle link_fd, SafeUnixHandle src_fd)
|
||||
{
|
||||
return Ioctl(link_fd, IoctlRequest.FICLONE, src_fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/NzbDrone.Mono/Interop/SafeUnixHandle.cs
Normal file
37
src/NzbDrone.Mono/Interop/SafeUnixHandle.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Runtime.ConstrainedExecution;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Permissions;
|
||||
using Mono.Unix.Native;
|
||||
|
||||
namespace NzbDrone.Mono.Interop
|
||||
{
|
||||
[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
|
||||
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
|
||||
internal sealed class SafeUnixHandle : SafeHandle
|
||||
{
|
||||
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
||||
private SafeUnixHandle()
|
||||
: base(new IntPtr(-1), true)
|
||||
{
|
||||
}
|
||||
|
||||
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
||||
public SafeUnixHandle(int fd)
|
||||
: base(new IntPtr(-1), true)
|
||||
{
|
||||
handle = new IntPtr(fd);
|
||||
}
|
||||
|
||||
public override bool IsInvalid
|
||||
{
|
||||
get { return handle == new IntPtr(-1); }
|
||||
}
|
||||
|
||||
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
return Syscall.close(handle.ToInt32()) != -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue