Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Common/src/Interop/Unix/System.Native/Interop.Stat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ internal struct FileStatus
internal long MTime;
internal long CTime;
internal long BirthTime;
internal long Dev;
internal long Ino;
}

internal static class FileTypes
Expand Down
2 changes: 2 additions & 0 deletions src/Native/Unix/System.Native/pal_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ static_assert(PAL_IN_ISDIR == IN_ISDIR, "");

static void ConvertFileStatus(const struct stat_& src, FileStatus* dst)
{
dst->Dev = static_cast<int64_t>(src.st_dev);
dst->Ino = static_cast<int64_t>(src.st_ino);
dst->Flags = FILESTATUS_FLAGS_NONE;
dst->Mode = static_cast<int32_t>(src.st_mode);
dst->Uid = src.st_uid;
Expand Down
2 changes: 2 additions & 0 deletions src/Native/Unix/System.Native/pal_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ struct FileStatus
int64_t MTime; // time of last modification
int64_t CTime; // time of last status change
int64_t BirthTime; // time the file was created
int64_t Dev; // ID of the device containing the file
int64_t Ino; // inode number of the file
};

/************
Expand Down
20 changes: 18 additions & 2 deletions src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,24 @@ public override void MoveFile(string sourceFullPath, string destFullPath)
{
// The desired behavior for Move(source, dest) is to not overwrite the destination file
// if it exists. Since rename(source, dest) will replace the file at 'dest' if it exists,
// link/unlink are used instead. Note that the Unix FileSystemWatcher will treat a Move
// as a Creation and Deletion instead of a Rename and thus differ from Windows.
// link/unlink are used instead. However, if the source path and the dest path refer to
// the same file, then do a rename rather than a link and an unlink. This is important
// for case-insensitive file systems (e.g. renaming a file in a way that just changes casing),
// so that we support changing the casing in the naming of the file. If this fails in any
// way (e.g. source file doesn't exist, dest file doesn't exist, rename fails, etc.), we
// just fall back to trying the link/unlink approach and generating any exceptional messages
// from there as necessary.
Interop.Sys.FileStatus sourceStat, destStat;
if (Interop.Sys.LStat(sourceFullPath, out sourceStat) == 0 && // source file exists
Interop.Sys.LStat(destFullPath, out destStat) == 0 && // dest file exists
sourceStat.Dev == destStat.Dev && // source and dest are on the same device
sourceStat.Ino == destStat.Ino && // and source and dest are the same file on that device
Interop.Sys.Rename(sourceFullPath, destFullPath) == 0) // try the rename
{
// Renamed successfully.
return;
}

if (Interop.Sys.Link(sourceFullPath, destFullPath) < 0)
{
// If link fails, we can fall back to doing a full copy, but we'll only do so for
Expand Down
28 changes: 28 additions & 0 deletions src/System.IO.FileSystem/tests/File/Move.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,34 @@ public void MoveIntoParentDirectory()
Assert.True(File.Exists(testFileDest.FullName));
}

[Fact]
public void MoveToSameName()
{
string testDir = GetTestFilePath();
Directory.CreateDirectory(testDir);

FileInfo testFileSource = new FileInfo(Path.Combine(testDir, GetTestFileName()));
testFileSource.Create().Dispose();

Move(testFileSource.FullName, testFileSource.FullName);
Assert.True(File.Exists(testFileSource.FullName));
}

[Fact]
public void MoveToSameNameDifferentCasing()
{
string testDir = GetTestFilePath();
Directory.CreateDirectory(testDir);

FileInfo testFileSource = new FileInfo(Path.Combine(testDir, Path.GetRandomFileName().ToLowerInvariant()));
testFileSource.Create().Dispose();

FileInfo testFileDest = new FileInfo(Path.Combine(testFileSource.DirectoryName, testFileSource.Name.ToUpperInvariant()));

Move(testFileSource.FullName, testFileDest.FullName);
Assert.True(File.Exists(testFileDest.FullName));
}

[Fact]
public void MultipleMoves()
{
Expand Down