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()
{
--- /dev/null
+// 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);
+ }
+ }
+}
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