mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Fix Move-Item to support cross-mount moves on Unix (#13044)
This commit is contained in:
@@ -2080,6 +2080,8 @@ namespace System.Management.Automation.Internal
|
||||
|
||||
internal static bool ShowMarkdownOutputBypass;
|
||||
|
||||
internal static bool ThrowExdevErrorOnMoveDirectory;
|
||||
|
||||
/// <summary>This member is used for internal test purposes.</summary>
|
||||
public static void SetTestHook(string property, object value)
|
||||
{
|
||||
|
||||
@@ -51,6 +51,12 @@ namespace Microsoft.PowerShell.Commands
|
||||
ISecurityDescriptorCmdletProvider,
|
||||
ICmdletProviderSupportsHelp
|
||||
{
|
||||
#if UNIX
|
||||
// This is the errno returned by the rename() syscall
|
||||
// when an item is attempted to be renamed across filesystem mount boundaries.
|
||||
private const int UNIX_ERRNO_EXDEV = 18;
|
||||
#endif
|
||||
|
||||
// 4MB gives the best results without spiking the resources on the remote connection for file transfers between pssessions.
|
||||
// NOTE: The script used to copy file data from session (PSCopyFromSessionHelper) has a
|
||||
// maximum fragment size value for security. If FILETRANSFERSIZE changes make sure the
|
||||
@@ -6001,15 +6007,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
|
||||
try
|
||||
{
|
||||
if (!IsSameVolume(directory.FullName, destination))
|
||||
{
|
||||
CopyAndDelete(directory, destination, force);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Move the file
|
||||
directory.MoveTo(destination);
|
||||
}
|
||||
MoveDirectoryInfoUnchecked(directory, destination, force);
|
||||
|
||||
WriteItemObject(
|
||||
directory,
|
||||
@@ -6027,14 +6025,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
directory.Attributes =
|
||||
directory.Attributes & ~(FileAttributes.ReadOnly | FileAttributes.Hidden);
|
||||
|
||||
if (!IsSameVolume(directory.FullName, destination))
|
||||
{
|
||||
CopyAndDelete(directory, destination, force);
|
||||
}
|
||||
else
|
||||
{
|
||||
directory.MoveTo(destination);
|
||||
}
|
||||
MoveDirectoryInfoUnchecked(directory, destination, force);
|
||||
|
||||
WriteItemObject(directory, directory.FullName, true);
|
||||
}
|
||||
@@ -6072,6 +6063,47 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements the file move operation for directories without handling any error scenarios.
|
||||
/// In particular, this attempts to rename or copy+delete the file,
|
||||
/// but passes any exceptional behavior through to the caller.
|
||||
/// </summary>
|
||||
/// <param name="directory">The directory to move.</param>
|
||||
/// <param name="destinationPath">The destination path to move the directory to.</param>
|
||||
/// <param name="force">If true, force move the directory, overwriting anything at the destination.</param>
|
||||
private void MoveDirectoryInfoUnchecked(DirectoryInfo directory, string destinationPath, bool force)
|
||||
{
|
||||
#if UNIX
|
||||
try
|
||||
{
|
||||
if (InternalTestHooks.ThrowExdevErrorOnMoveDirectory)
|
||||
{
|
||||
throw new IOException("Invalid cross-device link", hresult: UNIX_ERRNO_EXDEV);
|
||||
}
|
||||
|
||||
directory.MoveTo(destinationPath);
|
||||
}
|
||||
catch (IOException e) when (e.HResult == UNIX_ERRNO_EXDEV)
|
||||
{
|
||||
// Rather than try to ascertain whether we can rename a directory ahead of time,
|
||||
// it's both faster and more correct to try to rename it and fall back to copy/deleting it
|
||||
// See also: https://github.com/coreutils/coreutils/blob/439741053256618eb651e6d43919df29625b8714/src/mv.c#L212-L216
|
||||
CopyAndDelete(directory, destinationPath, force);
|
||||
}
|
||||
#else
|
||||
// On Windows, being able to rename vs copy/delete a file
|
||||
// is just a question of the drive
|
||||
if (IsSameWindowsVolume(directory.FullName, destinationPath))
|
||||
{
|
||||
directory.MoveTo(destinationPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyAndDelete(directory, destinationPath, force);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void CopyAndDelete(DirectoryInfo directory, string destination, bool force)
|
||||
{
|
||||
if (!ItemExists(destination))
|
||||
@@ -6107,13 +6139,15 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSameVolume(string source, string destination)
|
||||
#if !UNIX
|
||||
private bool IsSameWindowsVolume(string source, string destination)
|
||||
{
|
||||
FileInfo src = new FileInfo(source);
|
||||
FileInfo dest = new FileInfo(destination);
|
||||
|
||||
return (src.Directory.Root.Name == dest.Directory.Root.Name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion MoveItem
|
||||
|
||||
|
||||
@@ -131,12 +131,36 @@ Describe "Basic FileSystem Provider Tests" -Tags "CI" {
|
||||
"$destDir/$testDir/$testFile" | Should -Exist
|
||||
}
|
||||
|
||||
It "Verify Move-Item across devices for directory" {
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('ThrowExdevErrorOnMoveDirectory', $true)
|
||||
try
|
||||
{
|
||||
$dir = (New-Item -Path TestDrive:/dir -ItemType Directory -ErrorAction Stop).FullName
|
||||
$file = (New-Item -Path "$dir/file.txt" -Value "HELLO" -ErrorAction Stop).Name
|
||||
$destination = "$TestDrive/destination"
|
||||
|
||||
Move-Item -Path $dir -Destination $destination -ErrorAction Stop
|
||||
|
||||
$dir | Should -Not -Exist
|
||||
$destination | Should -Exist
|
||||
"$destination/$file" | Should -Exist
|
||||
}
|
||||
finally
|
||||
{
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('ThrowExdevErrorOnMoveDirectory', $false)
|
||||
}
|
||||
}
|
||||
|
||||
It "Verify Move-Item will not move to an existing file" {
|
||||
{ Move-Item -Path $testDir -Destination $testFile -ErrorAction Stop } | Should -Throw -ErrorId "MoveDirectoryItemIOError,Microsoft.PowerShell.Commands.MoveItemCommand"
|
||||
$error[0].Exception | Should -BeOfType System.IO.IOException
|
||||
$testDir | Should -Exist
|
||||
}
|
||||
|
||||
It "Verify Move-Item throws correct error for non-existent source" {
|
||||
{ Move-Item -Path /does/not/exist -Destination $testFile -ErrorAction Stop } | Should -Throw -ErrorId 'PathNotFound,Microsoft.PowerShell.Commands.MoveItemCommand'
|
||||
}
|
||||
|
||||
It "Verify Move-Item as substitute for Rename-Item" {
|
||||
$newFile = Move-Item -Path $testFile -Destination $newTestFile -PassThru
|
||||
$fileExists = Test-Path $newTestFile
|
||||
|
||||
Reference in New Issue
Block a user