8000 Enhance Remove-Item to work with OneDrive (Second) (#15260) · PowerShell/PowerShell@08e7313 · GitHub
[go: up one dir, main page]

Skip to content
Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 08e7313

Browse files
authored
Enhance Remove-Item to work with OneDrive (Second) (#15260)
1 parent 1df4236 commit 08e7313

File tree

5 files changed

+278
-149
lines changed

5 files changed

+278
-149
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 behavior 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: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,9 +1890,14 @@ 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+
// We've already taken the expense of initializing the Attributes property here,
1899+
// so we can use that to avoid needing to call IsReparsePointLikeSymlink() later.
1900+
checkReparsePoint = (recursiveDirectory.Attributes & FileAttributes.ReparsePoint) != 0;
18961901
}
18971902

18981903
// if "Hidden" is explicitly specified anywhere in the attribute filter, then override
@@ -1906,7 +1911,7 @@ private void Dir(
19061911
// c) it is not a reparse point with a target (not OneDrive or an AppX link).
19071912
if (tracker == null)
19081913
{
1909-
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(recursiveDirectory))
1914+
if (checkReparsePoint && InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(recursiveDirectory))
19101915
{
19111916
continue;
19121917
}
@@ -2058,7 +2063,7 @@ string ToModeString(FileSystemInfo fileSystemInfo)
20582063
public static string NameString(PSObject instance)
20592064
{
20602065
return instance?.BaseObject is FileSystemInfo fileInfo
2061-
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)
2066+
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(fileInfo)
20622067
? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"
20632068
: fileInfo.Name
20642069
: string.Empty;
@@ -3098,22 +3103,31 @@ private void RemoveDirectoryInfoItem(DirectoryInfo directory, bool recurse, bool
30983103
continueRemoval = ShouldProcess(directory.FullName, action);
30993104
}
31003105

3101-
if (directory.Attributes.HasFlag(FileAttributes.ReparsePoint))
3106+
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(directory))
31023107
{
3108+
void WriteErrorHelper(Exception exception)
3109+
{
3110+
WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
3111+
}
3112+
31033113
try
31043114
{
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();
3115+
if (InternalTestHooks.OneDriveTestOn)
3116+
{
3117+
WriteErrorHelper(new IOException());
3118+
return;
3119+
}
3120+
else
3121+
{
3122+
// Name surrogates should just be detached.
3123+
directory.Delete();
3124+
}
31113125
}
31123126
catch (Exception e)
31133127
{
31143128
string error = StringUtil.Format(FileSystemProviderStrings.CannotRemoveItem, directory.FullName, e.Message);
31153129
var exception = new IOException(error, e);
3116-
WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
3130+
WriteErrorHelper(exception);
31173131
}
31183132

31193133
return;
@@ -8024,7 +8038,7 @@ protected override bool ReleaseHandle()
80248038
}
80258039

80268040
// SetLastError is false as the use of this API doesn't not require GetLastError() to be called
8027-
[DllImport(PinvokeDllNames.FindFirstFileDllName, EntryPoint = "FindFirstFileExW", SetLastError = false, CharSet = CharSet.Unicode)]
8041+
[DllImport(PinvokeDllNames.FindFirs 10000 tFileDllName, EntryPoint = "FindFirstFileExW", SetLastError = true, CharSet = CharSet.Unicode)]
80288042
private static extern SafeFindHandle FindFirstFileEx(string lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, ref WIN32_FIND_DATA lpFindFileData, FINDEX_SEARCH_OPS fSearchOp, IntPtr lpSearchFilter, int dwAdditionalFlags);
80298043

80308044
internal enum FINDEX_INFO_LEVELS : uint
@@ -8215,28 +8229,50 @@ internal static bool IsReparsePoint(FileSystemInfo fileInfo)
82158229
return fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint);
82168230
}
82178231

8218-
internal static bool IsReparsePointWithTarget(FileSystemInfo fileInfo)
8232+
internal static bool IsReparsePointLikeSymlink(FileSystemInfo fileInfo)
82198233
{
8220-
if (!IsReparsePoint(fileInfo))
8234+
#if UNIX
8235+
// Reparse point on Unix is a symlink.
8236+
return IsReparsePoint(fileInfo);
8237+
#else
8238+
if (InternalTestHooks.OneDriveTestOn && fileInfo.Name == InternalTestHooks.OneDriveTestSymlinkName)
82218239
{
8222-
return false;
8240+
return !InternalTestHooks.OneDriveTestRecurseOn;
82238241
}
8224-
#if !UNIX
8225-
// It is a reparse point and we should check some reparse point tags.
8226-
var data = new WIN32_FIND_DATA();
8227-
using (var handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
8242+
8243+
WIN32_FIND_DATA data = default;
8244+
string fullPath = Path.TrimEndingDirectorySeparator(fileInfo.FullName);
8245+
using (var handle = FindFirstFileEx(fullPath, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
82288246
{
8247+
if (handle.IsInvalid)
8248+
{
8249+
// Our handle could be invalidated by something else touching the filesystem,
8250+
// so ensure we deal with that possibility here
8251+
int lastError = Marshal.GetLastWin32Error();
8252+
throw new Win32Exception(lastError);
8253+
}
8254+
8255+
// We already have the file attribute information from our Win32 call,
8256+
// so no need to take the expense of the FileInfo.FileAttributes call
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