Skip to content

Commit 8fb25a8

Browse files
hamarb123stephentoubdanmoseley
authored
Clone files on OSX-like platforms when possible, instead of copying the whole file (#79243)
Co-authored-by: Stephen Toub <stoub@microsoft.com> Co-authored-by: Dan Moseley <danmose@microsoft.com>
1 parent 90c5f05 commit 8fb25a8

File tree

5 files changed

+131
-9
lines changed

5 files changed

+131
-9
lines changed

src/libraries/Common/src/Interop/OSX/Interop.libc.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,10 @@ internal static unsafe int fsetattrlist(SafeHandle handle, AttrList* attrList, v
4444
handle.DangerousRelease();
4545
}
4646
}
47+
48+
[LibraryImport(Libraries.libc, EntryPoint = "clonefile", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)]
49+
internal static unsafe partial int clonefile(string src, string dst, int flags);
50+
51+
internal const int CLONE_ACL = 0x0004;
4752
}
4853
}

src/libraries/System.IO.FileSystem/tests/File/Copy.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,19 @@ public void WindowsAlternateDataStreamOverwrite(string defaultStream, string alt
352352
Assert.Throws<IOException>(() => Copy(testFileAlternateStream, testFile2 + alternateStream, overwrite: true));
353353
}
354354

355+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))]
356+
public void CopyOntoLockedFile()
357+
{
358+
string testFileSource = GetTestFilePath();
359+
string testFileDest = GetTestFilePath();
360+
File.Create(testFileSource).Dispose();
361+
File.Create(testFileDest).Dispose();
362+
using (var stream = new FileStream(testFileDest, FileMode.Open, FileAccess.Read, FileShare.None))
363+
{
364+
Assert.Throws<IOException>(() => Copy(testFileSource, testFileDest, overwrite: true));
365+
}
366+
}
367+
355368
[Fact]
356369
public void DestinationFileIsTruncatedWhenItsLargerThanSourceFile()
357370
{

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2434,6 +2434,7 @@
24342434
<Link>Common\Interop\OSX\Interop.libc.cs</Link>
24352435
</Compile>
24362436
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStatus.SetTimes.OSX.cs" />
2437+
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystem.TryCloneFile.OSX.cs" />
24372438
</ItemGroup>
24382439
<ItemGroup Condition="'$(IsiOSLike)' == 'true' or '$(IsOSXLike)' == 'true'">
24392440
<Compile Include="$(CommonPath)Interop\OSX\System.Native\Interop.SearchPath.cs">
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Win32.SafeHandles;
5+
using System.Diagnostics;
6+
7+
namespace System.IO
8+
{
9+
internal static partial class FileSystem
10+
{
11+
static partial void TryCloneFile(string sourceFullPath, string destFullPath, bool overwrite, ref bool cloned)
12+
{
13+
// This helper function calls out to clonefile, and returns the error.
14+
static bool TryCloneFile(string sourceFullPath, string destFullPath, int flags, out Interop.Error error)
15+
{
16+
if (Interop.@libc.clonefile(sourceFullPath, destFullPath, flags) == 0)
17+
{
18+
// Success.
19+
error = Interop.Error.SUCCESS;
20+
return true;
21+
}
22+
23+
error = Interop.Sys.GetLastError();
24+
return false;
25+
}
26+
27+
// Try to clone the file immediately, this will only succeed if the
28+
// destination doesn't exist, so we don't worry about locking for this one.
29+
int flags = Interop.@libc.CLONE_ACL;
30+
Interop.Error error;
31+
if (TryCloneFile(sourceFullPath, destFullPath, flags, out error))
32+
{
33+
cloned = true;
34+
return;
35+
}
36+
37+
// Some filesystems don't support ACLs, so may fail due to trying to copy ACLs.
38+
// This will disable them and allow trying again (a maximum of 1 time).
39+
if (error == Interop.Error.EINVAL)
40+
{
41+
flags = 0;
42+
if (TryCloneFile(sourceFullPath, destFullPath, flags, out error))
43+
{
44+
cloned = true;
45+
return;
46+
}
47+
}
48+
49+
// Try to delete the destination file if we're overwriting.
50+
if (error == Interop.Error.EEXIST && overwrite)
51+
{
52+
// Delete the destination. This should fail on directories. Get a lock to the dest file to ensure we don't copy onto it when
53+
// it's locked by something else, and then delete it. It should also fail if destination == source since it's already locked.
54+
try
55+
{
56+
using SafeFileHandle? dstHandle = SafeFileHandle.Open(destFullPath, FileMode.Open, FileAccess.ReadWrite,
57+
FileShare.None, FileOptions.None, preallocationSize: 0, createOpenException: CreateOpenExceptionForCopyFile);
58+
if (Interop.Sys.Unlink(destFullPath) < 0 &&
59+
Interop.Sys.GetLastError() != Interop.Error.ENOENT)
60+
{
61+
// Fall back to standard copy as an unexpected error has occurred.
62+
return;
63+
}
64+
}
65+
catch (FileNotFoundException)
66+
{
67+
// We don't want to throw if it's just the file not existing, since we're trying to delete it.
68+
}
69+
70+
// Try clonefile now we've deleted the destination file.
71+
if (TryCloneFile(sourceFullPath, destFullPath, flags, out error))
72+
{
73+
cloned = true;
74+
return;
75+
}
76+
}
77+
78+
if (error is Interop.Error.ENOTSUP // Check if it's not supported,
79+
or Interop.Error.EXDEV // if files are on different filesystems,
80+
or Interop.Error.EEXIST) // or if the destination file still exists.
81+
{
82+
// Fall back to normal copy.
83+
return;
84+
}
85+
86+
// Throw the appropriate exception.
87+
Debug.Assert(error != Interop.Error.EINVAL); // We shouldn't fail due to an invalid parameter.
88+
Debug.Assert(error != Interop.Error.SUCCESS); // We shouldn't fail with success.
89+
throw Interop.GetExceptionForIoErrno(error.Info(), destFullPath);
90+
}
91+
}
92+
}

src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,38 @@ internal static partial class FileSystem
2828
UnixFileMode.OtherWrite |
2929
UnixFileMode.OtherExecute;
3030

31+
static partial void TryCloneFile(string sourceFullPath, string destFullPath, bool overwrite, ref bool cloned);
32+
3133
public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite)
3234
{
3335
long fileLength;
3436
UnixFileMode filePermissions;
3537
using SafeFileHandle src = SafeFileHandle.OpenReadOnly(sourceFullPath, FileOptions.None, out fileLength, out filePermissions);
38+
39+
// Try to clone the file first.
40+
bool cloned = false;
41+
TryCloneFile(sourceFullPath, destFullPath, overwrite, ref cloned);
42+
if (cloned)
43+
{
44+
return;
45+
}
46+
3647
using SafeFileHandle dst = SafeFileHandle.Open(destFullPath, overwrite ? FileMode.Create : FileMode.CreateNew,
3748
FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize: 0, filePermissions,
38-
CreateOpenException);
49+
CreateOpenExceptionForCopyFile);
3950

4051
Interop.CheckIo(Interop.Sys.CopyFile(src, dst, fileLength));
52+
}
4153

42-
static Exception? CreateOpenException(Interop.ErrorInfo error, Interop.Sys.OpenFlags flags, string path)
54+
private static Exception? CreateOpenExceptionForCopyFile(Interop.ErrorInfo error, Interop.Sys.OpenFlags flags, string path)
55+
{
56+
// If the destination path points to a directory, we throw to match Windows behaviour.
57+
if (error.Error == Interop.Error.EEXIST && DirectoryExists(path))
4358
{
44-
// If the destination path points to a directory, we throw to match Windows behaviour.
45-
if (error.Error == Interop.Error.EEXIST && DirectoryExists(path))
46-
{
47-
return new IOException(SR.Format(SR.Arg_FileIsDirectory_Name, path));
48-
}
49-
50-
return null; // Let SafeFileHandle create the exception for this error.
59+
return new IOException(SR.Format(SR.Arg_FileIsDirectory_Name, path));
5160
}
61+
62+
return null; // Let SafeFileHandle create the exception for this error.
5263
}
5364

5465
#pragma warning disable IDE0060

0 commit comments

Comments
 (0)