From 31da98522183884e9570fe2e5257a75830258107 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Fri, 19 Feb 2021 16:28:34 -0800 Subject: [PATCH 1/4] Fix FileSystemProvider to not treat all reparsepoints as files --- .../commands/management/Navigation.cs | 2 +- .../namespaces/FileSystemProvider.cs | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs index 23aa621266d..692a2b49a7c 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs @@ -2699,7 +2699,7 @@ protected override void ProcessRecord() try { System.IO.DirectoryInfo di = new(providerPath); - if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0) + if (di != null && di.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint) && !di.Attributes.HasFlag(System.IO.FileAttributes.Directory)) { shouldRecurse = false; treatAsFile = true; diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index db619e3d15e..cf692978271 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -3108,12 +3108,7 @@ private void RemoveDirectoryInfoItem(DirectoryInfo directory, bool recurse, bool { 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(); + directory.Delete(recursive: recurse); } catch (Exception e) { From 491519a100397c5e0a0f8bb2068c7c0d7cd6e79e Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Fri, 19 Feb 2021 17:07:18 -0800 Subject: [PATCH 2/4] Fix logic so that a reparsepoint directory where -recurse is not specified is still treated as a file --- .../commands/management/Navigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs index 692a2b49a7c..5db021c1707 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs @@ -2699,7 +2699,7 @@ protected override void ProcessRecord() try { System.IO.DirectoryInfo di = new(providerPath); - if (di != null && di.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint) && !di.Attributes.HasFlag(System.IO.FileAttributes.Directory)) + if (di != null && di.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint) && (di.Attributes.HasFlag(System.IO.FileAttributes.Directory) && !shouldRecurse)) { shouldRecurse = false; treatAsFile = true; From e413e2c9422dcf93b43059ccbef345fdebdce10b Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Mon, 22 Feb 2021 10:05:50 -0800 Subject: [PATCH 3/4] Add more comments addressing Ilya's feedback --- .../commands/management/Navigation.cs | 2 +- .../namespaces/FileSystemProvider.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs index 5db021c1707..6d998220c20 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs @@ -2693,7 +2693,7 @@ protected override void ProcessRecord() bool shouldRecurse = Recurse; bool treatAsFile = false; - // only check if path is a directory using DirectoryInfo if using FileSystemProvider + // only check if path is a directory using DirectoryInfo if using FileSystemProvider, otherwise will fail for other providers like Registry if (resolvedPath.Provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase)) { try diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index cf692978271..a9c2e741ba0 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -3108,6 +3108,10 @@ private void RemoveDirectoryInfoItem(DirectoryInfo directory, bool recurse, bool { try { + // For non-reparse points, the behavior is to manually recurse so if `-Confirm` is used, then the user + // gets a confirmation at each level. Unfortunately, a reparse point behaves differently and requires + // the `Delete(recursive)` overload to successfully delete the folder (like a OneDrive folder). + // Symlinks are still treated as files. directory.Delete(recursive: recurse); } catch (Exception e) From 3920394b1b59496c49c344e903cde9143183f874 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL)" Date: Tue, 23 Feb 2021 13:20:23 -0800 Subject: [PATCH 4/4] Address Committee feedback making this an ExperimentalFeature and requiring -Force --- .../commands/management/Navigation.cs | 11 ++++++++++- .../ExperimentalFeature/ExperimentalFeature.cs | 4 ++++ .../namespaces/FileSystemProvider.cs | 17 ++++++++++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs index 6d998220c20..71025f0ae61 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs @@ -2699,7 +2699,16 @@ protected override void ProcessRecord() try { System.IO.DirectoryInfo di = new(providerPath); - if (di != null && di.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint) && (di.Attributes.HasFlag(System.IO.FileAttributes.Directory) && !shouldRecurse)) + + if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSForceRemoveReparsePoint)) + { + if (di != null && di.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint) && (di.Attributes.HasFlag(System.IO.FileAttributes.Directory) && !shouldRecurse)) + { + shouldRecurse = false; + treatAsFile = true; + } + } + else if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0) { shouldRecurse = false; treatAsFile = true; diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 5eeaf814e10..3caf14e6392 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -22,6 +22,7 @@ public class ExperimentalFeature internal const string EngineSource = "PSEngine"; internal const string PSAnsiProgressFeatureName = "PSAnsiProgress"; + internal const string PSForceRemoveReparsePoint = "PSForceRemoveReparsePoint"; #endregion @@ -136,6 +137,9 @@ static ExperimentalFeature() new ExperimentalFeature( name: PSAnsiProgressFeatureName, description: "Enable lightweight progress bar that leverages ANSI codes for rendering"), + new ExperimentalFeature( + name: PSForceRemoveReparsePoint, + description: "Enable -Force to remove reparsepoint folders like on OneDrive"), }; EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures); diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index a9c2e741ba0..bf551507597 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -3108,11 +3108,18 @@ private void RemoveDirectoryInfoItem(DirectoryInfo directory, bool recurse, bool { try { - // For non-reparse points, the behavior is to manually recurse so if `-Confirm` is used, then the user - // gets a confirmation at each level. Unfortunately, a reparse point behaves differently and requires - // the `Delete(recursive)` overload to successfully delete the folder (like a OneDrive folder). - // Symlinks are still treated as files. - directory.Delete(recursive: recurse); + if (force && ExperimentalFeature.IsEnabled(ExperimentalFeature.PSForceRemoveReparsePoint)) + { + // For non-reparse points, the behavior is to manually recurse so if `-Confirm` is used, then the user + // gets a confirmation at each level. Unfortunately, a reparse point behaves differently and requires + // the `Delete(recursive)` overload to successfully delete the folder (like a OneDrive folder). + // Symlinks are still treated as files. + directory.Delete(recursive: recurse); + } + else + { + directory.Delete(); + } } catch (Exception e) {