-
Notifications
You must be signed in to change notification settings - Fork 4.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve Directory.Move and DirectoryInfo.MoveTo #63675
Changes from all commits
b6ca0d9
43509b6
896245d
af1cd9b
373a4a2
02a8664
753e3d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -387,40 +387,58 @@ private static void CreateParentsAndDirectory(string fullPath) | |
} | ||
} | ||
|
||
public static void MoveDirectory(string sourceFullPath, string destFullPath) | ||
private static void MoveDirectory(string sourceFullPath, string destFullPath, bool sameDirectoryDifferentCase) | ||
{ | ||
// Windows doesn't care if you try and copy a file via "MoveDirectory"... | ||
if (FileExists(sourceFullPath)) | ||
{ | ||
// ... but it doesn't like the source to have a trailing slash ... | ||
ReadOnlySpan<char> srcNoDirectorySeparator = Path.TrimEndingDirectorySeparator(sourceFullPath.AsSpan()); | ||
ReadOnlySpan<char> destNoDirectorySeparator = Path.TrimEndingDirectorySeparator(destFullPath.AsSpan()); | ||
|
||
if (OperatingSystem.IsBrowser() && Path.EndsInDirectorySeparator(sourceFullPath) && FileExists(sourceFullPath)) | ||
{ | ||
// On Windows we end up with ERROR_INVALID_NAME, which is | ||
// "The filename, directory name, or volume label syntax is incorrect." | ||
// | ||
// This surfaces as an IOException, if we let it go beyond here it would | ||
// give DirectoryNotFound. | ||
|
||
if (Path.EndsInDirectorySeparator(sourceFullPath)) | ||
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); | ||
|
||
// ... but it doesn't care if the destination has a trailing separator. | ||
destFullPath = Path.TrimEndingDirectorySeparator(destFullPath); | ||
// On Unix, rename fails with ENOTDIR, but on WASM it does not. | ||
// So if the path ends with directory separator, but it's a file, we just throw. | ||
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); | ||
} | ||
|
||
if (FileExists(destFullPath)) | ||
if (!sameDirectoryDifferentCase) // This check is to allow renaming of directories | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because file systems are case sensitive I don't understand why we need a specific check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is something that already existed and to be honest I have not questioned that before you have asked the question. It seems that @wfurt agrees with you (#65059 (comment)):
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My guess is that this may be there to surface better exceptions. We could defer to actual IO but that may give different behaviors on different platforms. And Some OSes and file systems do care about case and some don't. |
||
{ | ||
// Some Unix distros will overwrite the destination file if it already exists. | ||
// Throwing IOException to match Windows behavior. | ||
throw new IOException(SR.Format(SR.IO_AlreadyExists_Name, destFullPath)); | ||
if (Interop.Sys.Stat(destNoDirectorySeparator, out _) >= 0) | ||
{ | ||
// destination exists, but before we throw we need to check whether source exists or not | ||
|
||
// Windows will throw if the source file/directory doesn't exist, we preemptively check | ||
// to make sure our cross platform behavior matches .NET Framework behavior. | ||
if (Interop.Sys.Stat(srcNoDirectorySeparator, out Interop.Sys.FileStatus sourceFileStatus) < 0) | ||
{ | ||
throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); | ||
} | ||
else if ((sourceFileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFDIR | ||
&& Path.EndsInDirectorySeparator(sourceFullPath)) | ||
{ | ||
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); | ||
} | ||
|
||
// Some Unix distros will overwrite the destination file if it already exists. | ||
// Throwing IOException to match Windows behavior. | ||
throw new IOException(SR.Format(SR.IO_AlreadyExists_Name, destFullPath)); | ||
} | ||
} | ||
|
||
if (Interop.Sys.Rename(sourceFullPath, destFullPath) < 0) | ||
if (Interop.Sys.Rename(sourceFullPath, destNoDirectorySeparator) < 0) | ||
{ | ||
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); | ||
switch (errorInfo.Error) | ||
{ | ||
case Interop.Error.EACCES: // match Win32 exception | ||
throw new IOException(SR.Format(SR.UnauthorizedAccess_IODenied_Path, sourceFullPath), errorInfo.RawErrno); | ||
case Interop.Error.ENOENT: | ||
throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, | ||
Interop.Sys.Stat(srcNoDirectorySeparator, out _) >= 0 | ||
? destFullPath // the source directory exists, so destination does not. Example: Move("/tmp/existing/", "/tmp/nonExisting1/nonExisting2/") | ||
: sourceFullPath)); | ||
case Interop.Error.ENOTDIR: // sourceFullPath exists and it's not a directory | ||
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath)); | ||
default: | ||
throw Interop.GetExceptionForIoErrno(errorInfo, isDirectory: true); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use explicit Dispose calls (instead of
using
-Statements)?