mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Enhance Remove-Item to work with OneDrive (#14902)
This commit is contained in:
@@ -2699,7 +2699,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
try
|
||||
{
|
||||
System.IO.DirectoryInfo di = new(providerPath);
|
||||
if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
|
||||
if (di != null && InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(di))
|
||||
{
|
||||
shouldRecurse = false;
|
||||
treatAsFile = true;
|
||||
|
||||
@@ -1863,7 +1863,7 @@ namespace System.Management.Automation
|
||||
|
||||
if (ExperimentalFeature.IsEnabled("PSAnsiRendering"))
|
||||
{
|
||||
PSStyle psstyle = PSStyle.Instance;
|
||||
PSStyle psstyle = PSStyle.Instance;
|
||||
switch (formatStyle)
|
||||
{
|
||||
case FormatStyle.Reset:
|
||||
@@ -2100,6 +2100,13 @@ namespace System.Management.Automation.Internal
|
||||
|
||||
internal static bool ThrowExdevErrorOnMoveDirectory;
|
||||
|
||||
// To emulate OneDrive behavior we use the hard-coded symlink.
|
||||
// If OneDriveTestRecuseOn is false then the symlink works as regular symlink.
|
||||
// If OneDriveTestRecuseOn is true then we resurce into the symlink as OneDrive should work.
|
||||
internal static bool OneDriveTestOn;
|
||||
internal static bool OneDriveTestRecurseOn;
|
||||
internal static string OneDriveTestSymlinkName = "link-Beta";
|
||||
|
||||
/// <summary>This member is used for internal test purposes.</summary>
|
||||
public static void SetTestHook(string property, object value)
|
||||
{
|
||||
|
||||
@@ -1890,9 +1890,17 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
|
||||
bool hidden = false;
|
||||
bool checkReparsePoint = true;
|
||||
if (!Force)
|
||||
{
|
||||
hidden = (recursiveDirectory.Attributes & FileAttributes.Hidden) != 0;
|
||||
|
||||
// Performance optimization.
|
||||
// Since we have already checked Attributes for Hidden we have already did a p/invoke
|
||||
// and initialized Attributes property.
|
||||
// So here we can check for ReparsePoint without new p/invoke.
|
||||
// If it is not a reparse point we skip one p/invoke in IsReparsePointLikeSymlink() below.
|
||||
checkReparsePoint = (recursiveDirectory.Attributes & FileAttributes.ReparsePoint) != 0;
|
||||
}
|
||||
|
||||
// if "Hidden" is explicitly specified anywhere in the attribute filter, then override
|
||||
@@ -1906,7 +1914,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
// c) it is not a reparse point with a target (not OneDrive or an AppX link).
|
||||
if (tracker == null)
|
||||
{
|
||||
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(recursiveDirectory))
|
||||
if (checkReparsePoint && InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(recursiveDirectory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -2058,7 +2066,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
public static string NameString(PSObject instance)
|
||||
{
|
||||
return instance?.BaseObject is FileSystemInfo fileInfo
|
||||
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)
|
||||
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(fileInfo)
|
||||
? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"
|
||||
: fileInfo.Name
|
||||
: string.Empty;
|
||||
@@ -3098,22 +3106,31 @@ namespace Microsoft.PowerShell.Commands
|
||||
continueRemoval = ShouldProcess(directory.FullName, action);
|
||||
}
|
||||
|
||||
if (directory.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(directory))
|
||||
{
|
||||
void WriteErrorHelper(Exception exception)
|
||||
{
|
||||
WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO:
|
||||
// Different symlinks seem to vary by behavior.
|
||||
// In particular, OneDrive symlinks won't remove without recurse,
|
||||
// but the .NET API here does not allow us to distinguish them.
|
||||
// We may need to revisit using p/Invokes here to get the right behavior
|
||||
directory.Delete();
|
||||
if (InternalTestHooks.OneDriveTestOn)
|
||||
{
|
||||
WriteErrorHelper(new IOException());
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Name surrogates should just be detached.
|
||||
directory.Delete();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string error = StringUtil.Format(FileSystemProviderStrings.CannotRemoveItem, directory.FullName, e.Message);
|
||||
var exception = new IOException(error, e);
|
||||
WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
|
||||
WriteErrorHelper(exception);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -8215,28 +8232,47 @@ namespace Microsoft.PowerShell.Commands
|
||||
return fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint);
|
||||
}
|
||||
|
||||
internal static bool IsReparsePointWithTarget(FileSystemInfo fileInfo)
|
||||
internal static bool IsReparsePointLikeSymlink(FileSystemInfo fileInfo)
|
||||
{
|
||||
if (!IsReparsePoint(fileInfo))
|
||||
#if UNIX
|
||||
// Reparse point on Unix is a symlink.
|
||||
return IsReparsePoint(fileInfo);
|
||||
#else
|
||||
if (InternalTestHooks.OneDriveTestOn && fileInfo.Name == InternalTestHooks.OneDriveTestSymlinkName)
|
||||
{
|
||||
return false;
|
||||
return !InternalTestHooks.OneDriveTestRecurseOn;
|
||||
}
|
||||
#if !UNIX
|
||||
// It is a reparse point and we should check some reparse point tags.
|
||||
var data = new WIN32_FIND_DATA();
|
||||
|
||||
WIN32_FIND_DATA data = default;
|
||||
using (var handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
|
||||
{
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
// If we can not open the file object we assume it's a symlink.
|
||||
return true;
|
||||
}
|
||||
|
||||
// To exclude one extra p/invoke in some scenarios
|
||||
// we don't check fileInfo.FileAttributes
|
||||
const int FILE_ATTRIBUTE_REPARSE_POINT = 0x0400;
|
||||
if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
|
||||
{
|
||||
// Not a reparse point.
|
||||
return false;
|
||||
}
|
||||
|
||||
// The name surrogate bit 0x20000000 is defined in https://docs.microsoft.com/windows/win32/fileio/reparse-point-tags
|
||||
// Name surrogates (0x20000000) are reparse points that point to other named entities local to the filesystem
|
||||
// (like symlinks and mount points).
|
||||
// In the case of OneDrive, they are not name surrogates and would be safe to recurse into.
|
||||
if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK))
|
||||
if ((data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static bool WinIsHardLink(FileSystemInfo fileInfo)
|
||||
|
||||
@@ -570,7 +570,7 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
|
||||
$omegaFile1 = Join-Path $omegaDir "OmegaFile1"
|
||||
$omegaFile2 = Join-Path $omegaDir "OmegaFile2"
|
||||
$betaDir = Join-Path $alphaDir "sub-Beta"
|
||||
$betaLink = Join-Path $alphaDir "link-Beta"
|
||||
$betaLink = Join-Path $alphaDir "link-Beta" # Don't change! The name is hard-coded in PowerShell for OneDrive tests.
|
||||
$betaFile1 = Join-Path $betaDir "BetaFile1.txt"
|
||||
$betaFile2 = Join-Path $betaDir "BetaFile2.txt"
|
||||
$betaFile3 = Join-Path $betaDir "BetaFile3.txt"
|
||||
@@ -623,6 +623,31 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
|
||||
$ci = Get-ChildItem $alphaLink -Recurse -Name
|
||||
$ci.Count | Should -BeExactly 7 # returns 10 - unexpectly recurce in link-alpha\link-Beta. See https://github.com/PowerShell/PowerShell/issues/11614
|
||||
}
|
||||
It "Get-ChildItem will recurse into emulated OneDrive directory" -Skip:(-not $IsWindows) {
|
||||
# The test depends on the files created in previous test:
|
||||
#New-Item -ItemType SymbolicLink -Path $alphaLink -Value $alphaDir
|
||||
#New-Item -ItemType SymbolicLink -Path $betaLink -Value $betaDir
|
||||
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $true)
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
|
||||
try
|
||||
{
|
||||
# '$betaDir' is a symlink - we don't follow symlinks
|
||||
# This emulates PowerShell 6.2 and below behavior.
|
||||
$ci = Get-ChildItem -Path $alphaDir -Recurse
|
||||
$ci.Count | Should -BeExactly 7
|
||||
|
||||
# Now we follow the symlink like on OneDrive.
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $true)
|
||||
$ci = Get-ChildItem -Path $alphaDir -Recurse
|
||||
$ci.Count | Should -BeExactly 10
|
||||
}
|
||||
finally
|
||||
{
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $false)
|
||||
}
|
||||
}
|
||||
It "Get-ChildItem will recurse into symlinks given -FollowSymlink, avoiding link loops" {
|
||||
New-Item -ItemType Directory -Path $gammaDir
|
||||
New-Item -ItemType SymbolicLink -Path $uponeLink -Value $betaDir
|
||||
@@ -744,6 +769,42 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
|
||||
$childB.Count | Should -BeExactly $childA.Count
|
||||
$childB.Name | Should -BeExactly $childA.Name
|
||||
}
|
||||
It "Remove-Item will recurse into emulated OneDrive directory" -Skip:(-not $IsWindows) {
|
||||
$alphaDir = Join-Path $TestDrive "sub-alpha2"
|
||||
$alphaLink = Join-Path $TestDrive "link-alpha2"
|
||||
$alphaFile1 = Join-Path $alphaDir "AlphaFile1.txt"
|
||||
$betaDir = Join-Path $alphaDir "sub-Beta"
|
||||
$betaLink = Join-Path $alphaDir "link-Beta"
|
||||
$betaFile1 = Join-Path $betaDir "BetaFile1.txt"
|
||||
|
||||
New-Item -ItemType Directory -Path $alphaDir > $null
|
||||
New-Item -ItemType File -Path $alphaFile1 > $null
|
||||
New-Item -ItemType Directory -Path $betaDir > $null
|
||||
New-Item -ItemType File -Path $betaFile1 > $null
|
||||
|
||||
New-Item -ItemType SymbolicLink -Path $alphaLink -Value $alphaDir > $null
|
||||
New-Item -ItemType SymbolicLink -Path $betaLink -Value $betaDir > $null
|
||||
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $true)
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
|
||||
try
|
||||
{
|
||||
# With the test hook turned on we don't remove '$betaDir' symlink.
|
||||
# This emulates PowerShell 7.1 and below behavior.
|
||||
{ Remove-Item -Path $betaLink -Recurse -ErrorAction Stop } | Should -Throw -ErrorId "DeleteSymbolicLinkFailed,Microsoft.PowerShell.Commands.RemoveItemCommand"
|
||||
|
||||
# Now we emulate OneDrive and follow the symlink like on OneDrive.
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $true)
|
||||
Remove-Item -Path $betaLink -Recurse
|
||||
Test-Path -Path $betaLink | Should -BeFalse
|
||||
}
|
||||
finally
|
||||
{
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
|
||||
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user