8000 Enhance Remove-Item to work with OneDrive (#14902) · rkeithhill/PowerShell@fafc38f · GitHub
[go: up one dir, main page]

Skip to content

Commit fafc38f

Browse files
authored
Enhance Remove-Item to work with OneDrive (PowerShell#14902)
1 parent e33a3c7 commit fafc38f

File tree

4 files changed

+125
-21
lines changed

4 files changed

+125
-21
lines changed

src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2699,7 +2699,7 @@ protected override void ProcessRecord()
26992699
try
27002700
{
27012701
System.IO.DirectoryInfo di = new(providerPath);
2702-
if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
2702+
if (di != null && InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(di))
27032703
{
27042704
shouldRecurse = false;
27052705
treatAsFile = true;

src/System.Management.Automation/engine/Utils.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1863,7 +1863,7 @@ internal static string GetFormatStyleString(FormatStyle formatStyle)
18631863

18641864
if (ExperimentalFeature.IsEnabled("PSAnsiRendering"))
18651865
{
1866-
PSStyle psstyle = PSStyle.Instance;
1866+
PSStyle psstyle = PSStyle.Instance;
18671867
switch (formatStyle)
18681868
{
18691869
case FormatStyle.Reset:
@@ -2100,6 +2100,13 @@ public static class InternalTestHooks
21002100

21012101
internal static bool ThrowExdevErrorOnMoveDirectory;
21022102

2103+
// To emulate OneDrive behavio 8000 r we use the hard-coded symlink.
2104+
// If OneDriveTestRecuseOn is false then the symlink works as regular symlink.
2105+
// If OneDriveTestRecuseOn is true then we resurce into the symlink as OneDrive should work.
2106+
internal static bool OneDriveTestOn;
2107+
internal static bool OneDriveTestRecurseOn;
2108+
internal static string OneDriveTestSymlinkName = "link-Beta";
2109+
21032110
/// <summary>This member is used for internal test purposes.</summary>
21042111
public static void SetTestHook(string property, object value)
21052112
{

src/System.Management.Automation/namespaces/FileSystemProvider.cs

Lines changed: 54 additions & 18 deletions
6D40
Original file line numberDiff line numberDiff line change
@@ -1890,9 +1890,17 @@ private void Dir(
18901890
}
18911891

18921892
bool hidden = false;
1893+
bool checkReparsePoint = true;
18931894
if (!Force)
18941895
{
18951896
hidden = (recursiveDirectory.Attributes & FileAttributes.Hidden) != 0;
1897+
1898+
// Performance optimization.
1899+
// Since we have already checked Attributes for Hidden we have already did a p/invoke
1900+
// and initialized Attributes property.
1901+
// So here we can check for ReparsePoint without new p/invoke.
1902+
// If it is not a reparse point we skip one p/invoke in IsReparsePointLikeSymlink() below.
1903+
checkReparsePoint = (recursiveDirectory.Attributes & FileAttributes.ReparsePoint) != 0;
18961904
}
18971905

18981906
// if "Hidden" is explicitly specified anywhere in the attribute filter, then override
@@ -1906,7 +1914,7 @@ private void Dir(
19061914
// c) it is not a reparse point with a target (not OneDrive or an AppX link).
19071915
if (tracker == null)
19081916
{
1909-
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(recursiveDirectory))
1917+
if (checkReparsePoint && InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(recursiveDirectory))
19101918
{
19111919
continue;
19121920
}
@@ -2058,7 +2066,7 @@ string ToModeString(FileSystemInfo fileSystemInfo)
20582066
public static string NameString(PSObject instance)
20592067
{
20602068
return instance?.BaseObject is FileSystemInfo fileInfo
2061-
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)
2069+
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(fileInfo)
20622070
? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"
20632071
: fileInfo.Name
20642072
: string.Empty;
@@ -3098,22 +3106,31 @@ private void RemoveDirectoryInfoItem(DirectoryInfo directory, bool recurse, bool
30983106
continueRemoval = ShouldProcess(directory.FullName, action);
30993107
}
31003108

3101-
if (directory.Attributes.HasFlag(FileAttributes.ReparsePoint))
3109+
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(directory))
31023110
{
3111+
void WriteErrorHelper(Exception exception)
3112+
{
3113+
WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
3114+
}
3115+
31033116
try
31043117
{
3105-
// TODO:
3106-
// Different symlinks seem to vary by behavior.
3107-
// In particular, OneDrive symlinks won't remove without recurse,
3108-
// but the .NET API here does not allow us to distinguish them.
3109-
// We may need to revisit using p/Invokes here to get the right behavior
3110-
directory.Delete();
3118+
if (InternalTestHooks.OneDriveTestOn)
3119+
{
3120+
WriteErrorHelper(new IOException());
3121+
return;
3122+
}
3123+
else
3124+
{
3125+
// Name surrogates should just be detached.
3126+
directory.Delete();
3127+
}
31113128
}
31123129
catch (Exception e)
31133130
{
31143131
string error = StringUtil.Format(FileSystemProviderStrings.CannotRemoveItem, directory.FullName, e.Message);
31153132
var exception = new IOException(error, e);
3116-
WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
3133+
WriteErrorHelper(exception);
31173134
}
31183135

31193136
return;
@@ -8215,28 +8232,47 @@ internal static bool IsReparsePoint(FileSystemInfo fileInfo)
82158232
return fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint);
82168233
}
82178234

8218-
internal static bool IsReparsePointWithTarget(FileSystemInfo fileInfo)
8235+
internal static bool IsReparsePointLikeSymlink(FileSystemInfo fileInfo)
82198236
{
8220-
if (!IsReparsePoint(fileInfo))
8237+
#if UNIX
8238+
// Reparse point on Unix is a symlink.
8239+
return IsReparsePoint(fileInfo);
8240+
#else
8241+
if (InternalTestHooks.OneDriveTestOn && fileInfo.Name == InternalTestHooks.OneDriveTestSymlinkName)
82218242
{
8222-
return false;
8243+
return !InternalTestHooks.OneDriveTestRecurseOn;
82238244
}
8224-
#if !UNIX
8225-
// It is a reparse point and we should check some reparse point tags.
8226-
var data = new WIN32_FIND_DATA();
8245+
8246+
WIN32_FIND_DATA data = default;
82278247
using (var handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
82288248
{
8249+
if (handle.IsInvalid)
8250+
{
8251+
// If we can not open the file object we assume it's a symlink.
8252+
return true;
8253+
}
8254+
8255+
// To exclude one extra p/invoke in some scenarios
8256+
// we don't check fileInfo.FileAttributes
8257+
const int FILE_ATTRIBUTE_REPARSE_POINT = 0x0400;
8258+
if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
8259+
{
8260+
// Not a reparse point.
8261+
return false;
8262+
}
8263+
82298264
// The name surrogate bit 0x20000000 is defined in https://docs.microsoft.com/windows/win32/fileio/reparse-point-tags
82308265
// Name surrogates (0x20000000) are reparse points that point to other named entities local to the filesystem
82318266
// (like symlinks and mount points).
82328267
// In the case of OneDrive, they are not name surrogates and would be safe to recurse into.
8233-
if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK))
8268+
if ((data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK))
82348269
{
82358270
return false;
82368271
}
82378272
}
8238-
#endif
8273+
82398274
return true;
8275+
#endif
82408276
}
82418277

82428278
internal static bool WinIsHardLink(FileSystemInfo fileInfo)

test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
570570
$omegaFile1 = Join-Path $omegaDir "OmegaFile1"
571571
$omegaFile2 = Join-Path $omegaDir "OmegaFile2"
572572
$betaDir = Join-Path $alphaDir "sub-Beta"
573-
$betaLink = Join-Path $alphaDir "link-Beta"
573+
$betaLink = Join-Path $alphaDir "link-Beta" # Don't change! The name is hard-coded in PowerShell for OneDrive tests.
574574
$betaFile1 = Join-Path $betaDir "BetaFile1.txt"
575575
$betaFile2 = Join-Path $betaDir "BetaFile2.txt"
576576
$betaFile3 = Join-Path $betaDir "BetaFile3.txt"
@@ -623,6 +623,31 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
623623
$ci = Get-ChildItem $alphaLink -Recurse -Name
624624
$ci.Count | Should -BeExactly 7 # returns 10 - unexpectly recurce in link-alpha\link-Beta. See https://github.com/PowerShell/PowerShell/issues/11614
625625
}
626+
It "Get-ChildItem will recurse into emulated OneDrive directory" -Skip:(-not $IsWindows) {
627+
# The test depends on the files created in previous test:
628+
#New-Item -ItemType SymbolicLink -Path $alphaLink -Value $alphaDir
629+
#New-Item -ItemType SymbolicLink -Path $betaLink -Value $betaDir
630+
631+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $true)
632+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
633+
try
634+
{
635+
# '$betaDir' is a symlink - we don't follow symlinks
636+
# This emulates PowerShell 6.2 and below behavior.
637+
$ci = Get-ChildItem -Path $alphaDir -Recurse
638+
$ci.Count | Should -BeExactly 7
639+
640+
# Now we follow the symlink like on OneDrive.
641+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $true)
642+
$ci = Get-ChildItem -Path $alphaDir -Recurse
643+
$ci.Count | Should -BeExactly 10
644+
}
645+
finally
646+
{
647+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
648+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $false)
649+
}
650+
}
626651
It "Get-ChildItem will recurse into symlinks given -FollowSymlink, avoiding link loops" {
627652
New-Item -ItemType Directory -Path $gammaDir
628653
New-Item -ItemType SymbolicLink -Path $uponeLink -Value $betaDir
@@ -744,6 +769,42 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
744769
$childB.Count | Should -BeExactly $childA.Count
745770
$childB.Name | Should -BeExactly $childA.Name
746771
}
772+
It "Remove-Item will recurse into emulated OneDrive directory" -Skip:(-not $IsWindows) {
773+
$alphaDir = Join-Path $TestDrive "sub-alpha2"
774+
$alphaLink = Join-Path $TestDrive "link-alpha2"
775+
$alphaFile1 = Join-Path $alphaDir "AlphaFile1.txt"
776+
$betaDir = Join-Path $alphaDir "sub-Beta"
777+
$betaLink = Join-Path $alphaDir "link-Beta"
778+
$betaFile1 = Join-Path $betaDir "BetaFile1.txt"
779+
780+
New-Item -ItemType Directory -Path $alphaDir > $null
781+
New-Item -ItemType File -Path $alphaFile1 > $null
782+
New-Item -ItemType Directory -Path $betaDir > $null
783+
New-Item -ItemType File -Path $betaFile1 > $null
784+
785+
New-Item -ItemType SymbolicLink -Path $alphaLink -Value $alphaDir > $null
786+
New-Item -ItemType SymbolicLink -Path $betaLink -Value $betaDir > $null
787+
788+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $true)
789+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
790+
try
791+
{
792+
# With the test hook turned on we don't remove '$betaDir' symlink.
793+
# This emulates PowerShell 7.1 and below behavior.
794+
{ Remove-Item -Path $betaLink -Recurse -ErrorAction Stop } | Should -Throw -ErrorId "DeleteSymbolicLinkFailed,Microsoft.PowerShell.Commands.RemoveItemCommand"
795+
796+
# Now we emulate OneDrive and follow the symlink like on OneDrive.
797+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $true)
798+
Remove-Item -Path $betaLink -Recurse
799+
Test-Path -Path $betaLink | Should -BeFalse
800+
}
801+
finally
802+
{
803+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
804+
[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $false)
805+
}
806+
}
807+
747808
}
748809
}
749810

0 commit comments

Comments
 (0)
0