Skip to content

Commit

Permalink
Clone files on OSX-like platforms when possible, instead of copying t…
Browse files Browse the repository at this point in the history
…he whole file (#79243)

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Dan Moseley <danmose@microsoft.com>
  • Loading branch information
3 people committed Jun 13, 2023
1 parent 90c5f05 commit 8fb25a8
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 9 deletions.
5 changes: 5 additions & 0 deletions src/libraries/Common/src/Interop/OSX/Interop.libc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,10 @@ internal static unsafe int fsetattrlist(SafeHandle handle, AttrList* attrList, v
handle.DangerousRelease();
}
}

[LibraryImport(Libraries.libc, EntryPoint = "clonefile", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)]
internal static unsafe partial int clonefile(string src, string dst, int flags);

internal const int CLONE_ACL = 0x0004;
}
}
13 changes: 13 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/File/Copy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,19 @@ public void WindowsAlternateDataStreamOverwrite(string defaultStream, string alt
Assert.Throws<IOException>(() => Copy(testFileAlternateStream, testFile2 + alternateStream, overwrite: true));
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))]
public void CopyOntoLockedFile()
{
string testFileSource = GetTestFilePath();
string testFileDest = GetTestFilePath();
File.Create(testFileSource).Dispose();
File.Create(testFileDest).Dispose();
using (var stream = new FileStream(testFileDest, FileMode.Open, FileAccess.Read, FileShare.None))
{
Assert.Throws<IOException>(() => Copy(testFileSource, testFileDest, overwrite: true));
}
}

[Fact]
public void DestinationFileIsTruncatedWhenItsLargerThanSourceFile()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2434,6 +2434,7 @@
<Link>Common\Interop\OSX\Interop.libc.cs</Link>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStatus.SetTimes.OSX.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystem.TryCloneFile.OSX.cs" />
</ItemGroup>
<ItemGroup Condition="'$(IsiOSLike)' == 'true' or '$(IsOSXLike)' == 'true'">
<Compile Include="$(CommonPath)Interop\OSX\System.Native\Interop.SearchPath.cs">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Win32.SafeHandles;
using System.Diagnostics;

namespace System.IO
{
internal static partial class FileSystem
{
static partial void TryCloneFile(string sourceFullPath, string destFullPath, bool overwrite, ref bool cloned)
{
// This helper function calls out to clonefile, and returns the error.
static bool TryCloneFile(string sourceFullPath, string destFullPath, int flags, out Interop.Error error)
{
if (Interop.@libc.clonefile(sourceFullPath, destFullPath, flags) == 0)
{
// Success.
error = Interop.Error.SUCCESS;
return true;
}

error = Interop.Sys.GetLastError();
return false;
}

// Try to clone the file immediately, this will only succeed if the
// destination doesn't exist, so we don't worry about locking for this one.
int flags = Interop.@libc.CLONE_ACL;
Interop.Error error;
if (TryCloneFile(sourceFullPath, destFullPath, flags, out error))
{
cloned = true;
return;
}

// Some filesystems don't support ACLs, so may fail due to trying to copy ACLs.
// This will disable them and allow trying again (a maximum of 1 time).
if (error == Interop.Error.EINVAL)
{
flags = 0;
if (TryCloneFile(sourceFullPath, destFullPath, flags, out error))
{
cloned = true;
return;
}
}

// Try to delete the destination file if we're overwriting.
if (error == Interop.Error.EEXIST && overwrite)
{
// Delete the destination. This should fail on directories. Get a lock to the dest file to ensure we don't copy onto it when
// it's locked by something else, and then delete it. It should also fail if destination == source since it's already locked.
try
{
using SafeFileHandle? dstHandle = SafeFileHandle.Open(destFullPath, FileMode.Open, FileAccess.ReadWrite,
FileShare.None, FileOptions.None, preallocationSize: 0, createOpenException: CreateOpenExceptionForCopyFile);
if (Interop.Sys.Unlink(destFullPath) < 0 &&
Interop.Sys.GetLastError() != Interop.Error.ENOENT)
{
// Fall back to standard copy as an unexpected error has occurred.
return;
}
}
catch (FileNotFoundException)
{
// We don't want to throw if it's just the file not existing, since we're trying to delete it.
}

// Try clonefile now we've deleted the destination file.
if (TryCloneFile(sourceFullPath, destFullPath, flags, out error))
{
cloned = true;
return;
}
}

if (error is Interop.Error.ENOTSUP // Check if it's not supported,
or Interop.Error.EXDEV // if files are on different filesystems,
or Interop.Error.EEXIST) // or if the destination file still exists.
{
// Fall back to normal copy.
return;
}

// Throw the appropriate exception.
Debug.Assert(error != Interop.Error.EINVAL); // We shouldn't fail due to an invalid parameter.
Debug.Assert(error != Interop.Error.SUCCESS); // We shouldn't fail with success.
throw Interop.GetExceptionForIoErrno(error.Info(), destFullPath);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,38 @@ internal static partial class FileSystem
UnixFileMode.OtherWrite |
UnixFileMode.OtherExecute;

static partial void TryCloneFile(string sourceFullPath, string destFullPath, bool overwrite, ref bool cloned);

public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite)
{
long fileLength;
UnixFileMode filePermissions;
using SafeFileHandle src = SafeFileHandle.OpenReadOnly(sourceFullPath, FileOptions.None, out fileLength, out filePermissions);

// Try to clone the file first.
bool cloned = false;
TryCloneFile(sourceFullPath, destFullPath, overwrite, ref cloned);
if (cloned)
{
return;
}

using SafeFileHandle dst = SafeFileHandle.Open(destFullPath, overwrite ? FileMode.Create : FileMode.CreateNew,
FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize: 0, filePermissions,
CreateOpenException);
CreateOpenExceptionForCopyFile);

Interop.CheckIo(Interop.Sys.CopyFile(src, dst, fileLength));
}

static Exception? CreateOpenException(Interop.ErrorInfo error, Interop.Sys.OpenFlags flags, string path)
private static Exception? CreateOpenExceptionForCopyFile(Interop.ErrorInfo error, Interop.Sys.OpenFlags flags, string path)
{
// If the destination path points to a directory, we throw to match Windows behaviour.
if (error.Error == Interop.Error.EEXIST && DirectoryExists(path))
{
// If the destination path points to a directory, we throw to match Windows behaviour.
if (error.Error == Interop.Error.EEXIST && DirectoryExists(path))
{
return new IOException(SR.Format(SR.Arg_FileIsDirectory_Name, path));
}

return null; // Let SafeFileHandle create the exception for this error.
return new IOException(SR.Format(SR.Arg_FileIsDirectory_Name, path));
}

return null; // Let SafeFileHandle create the exception for this error.
}

#pragma warning disable IDE0060
Expand Down

0 comments on commit 8fb25a8

Please sign in to comment.