dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.55k stars 4.54k forks source link

File.Copy copies through a symlink #80832

Open Forgind opened 1 year ago

Forgind commented 1 year ago

Description

See https://github.com/dotnet/msbuild/issues/8273

It appears that when passed a symlink, File.Copy overwrites the file at the other end of the symlink rather than overwriting the symlink. Is this intentional behavior? If so, perhaps there should be an overload that just overwrites the symlink?

Reproduction Steps

(From the issue above)

  1. Build an executable project with hard or symbolic links enabled
  2. Update the version of referenced NuGet package
  3. Build the application again, but this time, without using hard/symbolic links. MSBuild will try to update dependencies in the application's output folder. Sadly, instead of replacing existing links, it replaces actual files in NuGet cache (thus corrupting it).

In both cases file newtonsoft.json\13.0.1\lib\netstandard2.0\Newtonsoft.Json.dll is silently replaced with newtonsoft.json\13.0.2\lib\net6.0\Newtonsoft.Json.dll:

Expected behavior

Files in the NuGet cache remain untouched.

Actual behavior

Files in a NuGet package are silently replaced with files from another version.

Regression?

No response

Known Workarounds

Delete the file before calling Copy

Configuration

No response

Other information

No response

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-io See info in area-owners.md if you want to be subscribed.

Issue Details
### Description See https://github.com/dotnet/msbuild/issues/8273 It appears that when passed a symlink, File.Copy overwrites the file at the other end of the symlink rather than overwriting the symlink. Is this intentional behavior? If so, perhaps there should be an overload that just overwrites the symlink? ### Reproduction Steps (From the issue above) 1. Build an executable project with hard or symbolic links enabled 2. Update the version of referenced NuGet package 3. Build the application again, but this time, without using hard/symbolic links. MSBuild will try to update dependencies in the application's output folder. Sadly, instead of replacing existing links, it replaces actual files in NuGet cache (thus corrupting it). - hard-links ``` dotnet nuget locals --clear all dotnet new console dotnet add package newtonsoft.json -v 13.0.1 dotnet build /p:CreateHardLinksForCopyLocalIfPossible=true dotnet add package newtonsoft.json -v 13.0.2 dotnet build ``` - symbolic-links ``` dotnet nuget locals --clear all dotnet new console dotnet add package newtonsoft.json -v 13.0.1 dotnet build /p:CreateSymbolicLinksForCopyLocalIfPossible=true dotnet add package newtonsoft.json -v 13.0.2 dotnet build ``` In both cases file `newtonsoft.json\13.0.1\lib\netstandard2.0\Newtonsoft.Json.dll` is silently replaced with `newtonsoft.json\13.0.2\lib\net6.0\Newtonsoft.Json.dll`: ### Expected behavior Files in the NuGet cache remain untouched. ### Actual behavior Files in a NuGet package are silently replaced with files from another version. ### Regression? _No response_ ### Known Workarounds Delete the file before calling Copy ### Configuration _No response_ ### Other information _No response_
Author: Forgind
Assignees: -
Labels: `area-System.IO`
Milestone: -
Jozkee commented 1 year ago

Small repro:

namespace symlink_copy;
class Program
{
    static void Main(string[] args)
    {
        // clean-up previous run
        File.Delete("foo");
        File.Delete("linkToFoo");

        File.WriteAllText("foo", "This is foo's content");
        File.CreateSymbolicLink("linkToFoo", "foo");

        ReadAllText("foo");
        ReadAllText("linkToFoo");

        // now, copy bar to linkToFoo. 
        File.Create("bar").Dispose();
        File.Copy("bar", "linkToFoo", overwrite: true);

        // print again
        ReadAllText("foo");
        ReadAllText("linkToFoo");
    }

    static void ReadAllText(string path)
    {
        Console.WriteLine($"{path}'s content: " + File.ReadAllText(path));
    }
}

Note: File.Move does not replace link's target.

am11 commented 1 year ago

Note that this is the same behavior as cp command in linux without --remove-destination https://stackoverflow.com/q/9371222/863980 (and probably the underlying syscalls?). If it was intentional (which seems likely), then overload solution sounds better. cc @tmds

tmds commented 1 year ago

Yes, the behavior is intentional.

Note that with .NET 7 on Linux, File.Copy will perform copy-on-write clones on file systems that support it (like Btrfs or XFS). That matches the behavior that is desired here: avoid copying the data. (PR: https://github.com/dotnet/runtime/pull/59695)

manfred-brands commented 1 year ago

Does that mean the behaviour becomes filesystem dependent?

tmds commented 1 year ago

The functional behavior is the same. The performance is better on file systems that support the copy-on-write clone because we avoid copying the data.

am11 commented 1 year ago

Yes, the functional behavior is the same on macOS. Output of @Jozkee's program on osx-arm64 is also:

foo's content: This is foo's content
linkToFoo's content: This is foo's content
foo's content: 
linkToFoo's content: