优化递归修改网络共享文件ACL的PowerShell脚本请求
First off, nice work getting a functional script together—dealing with post-migration ACL messes is no fun, especially when you're staring at hundreds of thousands of files. The biggest bottleneck in your current approach is the per-item iteration with repeated Get-Item calls, costly try/catch blocks, and Get-ChildItem's memory-heavy handling of large datasets. Let's fix that to get speed closer to the GUI's native performance.
Key Optimizations to Implement
1. Use .NET's Streaming File Enumeration Instead of Get-ChildItem
Get-ChildItem -Recurse loads every file/folder object into memory upfront, which is slow and resource-heavy for large shares. Instead, use Directory.EnumerateFileSystemEntries with long-path syntax—it streams items one at a time and handles paths over 256 characters natively without extra checks.
2. Eliminate Redundant Filesystem Calls
Your script calls Get-Item multiple times per path (once to check existence, once in the catch block, etc.). Each call hits the filesystem, adding significant overhead. We'll infer item type (file/folder) directly from enumeration data to avoid repeated IO operations.
3. Reuse Pre-Configured Objects
Creating DirectorySecurity and FileSecurity objects inside the process block means reinitializing them for every item—move these to the BEGIN block to reuse them across all iterations.
4. Minimize Try/Catch Overhead
Try/catch blocks have a performance cost when triggered frequently. Instead of trying to apply ACL first and catching failures, proactively set ownership upfront (safe for a migrated share) to avoid repeated error handling.
5. Use .NET Methods Directly
PowerShell's Set-Acl wrapper has extra overhead; using .NET's SetAccessControl methods directly on files/folders speeds up operations.
Optimized Script
$CSScriptBlock = @' using System; using System.Runtime.InteropServices; public class PrivilegeAdjuster { [DllImport("advapi32.dll", SetLastError = true)] private static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid); private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; private const uint TOKEN_QUERY = 0x0008; private const uint SE_PRIVILEGE_ENABLED = 0x00000002; private const uint SE_PRIVILEGE_DISABLED = 0x00000000; [StructLayout(LayoutKind.Sequential)] private struct LUID { public uint LowPart; public int HighPart; } [StructLayout(LayoutKind.Sequential)] private struct TOKEN_PRIVILEGES { public uint PrivilegeCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; } [StructLayout(LayoutKind.Sequential)] private struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; } public static bool AdjustPrivilege(string privilegeName, bool enable) { IntPtr hToken; if (!OpenProcessToken(System.Diagnostics.Process.GetCurrentProcess().Handle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hToken)) { return false; } LUID luid; if (!LookupPrivilegeValue(null, privilegeName, out luid)) { return false; } TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES(); tp.PrivilegeCount = 1; tp.Privileges = new LUID_AND_ATTRIBUTES[1]; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = enable ? SE_PRIVILEGE_ENABLED : SE_PRIVILEGE_DISABLED; bool result = AdjustTokenPrivileges(hToken, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); return result; } } '@ function Adjust-Privilege { param( [Parameter(Mandatory=$true)] [string[]]$PrivilegeName, [switch]$Disable ) if (-not ([System.Management.Automation.PSTypeName]'PrivilegeAdjuster').Type) { Add-Type -TypeDefinition $CSScriptBlock } foreach ($priv in $PrivilegeName) { [PrivilegeAdjuster]::AdjustPrivilege($priv, -not $Disable) | Out-Null } } $root = "\\server\share\folder" # Convert root to long-path syntax upfront $longRoot = "\\?\UNC" + $root.Substring(2) function Restore-Inheritance { param( [Parameter(Mandatory=$true)] [string]$RootPath ) BEGIN { # Enable required privileges once "SeRestorePrivilege","SeBackupPrivilege","SeTakeOwnershipPrivilege" | Adjust-Privilege # Prepare root ACL (strip explicit ACEs once) $rootAcl = [System.Security.AccessControl.DirectorySecurity]::new($RootPath) $rootAcl.Access | Where-Object { -not $_.IsInherited } | ForEach-Object { $rootAcl.RemoveAccessRule($_) | Out-Null } # Pre-configure owner ACLs (reuse for all items) $adminAccount = [System.Security.Principal.NTAccount]'Builtin\Administrators' $dirOwnerAcl = [System.Security.AccessControl.DirectorySecurity]::new() $dirOwnerAcl.SetOwner($adminAccount) $fileOwnerAcl = [System.Security.AccessControl.FileSecurity]::new() $fileOwnerAcl.SetOwner($adminAccount) } PROCESS { # Stream items with .NET enumeration (no memory bloat) $enumerationOptions = [System.IO.EnumerationOptions]::new() $enumerationOptions.RecurseSubdirectories = $true $enumerationOptions.ReturnSpecialDirectories = $false foreach ($itemPath in [System.IO.Directory]::EnumerateFileSystemEntries($RootPath, "*", $enumerationOptions)) { Write-Host "Processing: $itemPath" try { $isDirectory = [System.IO.Directory]::Exists($itemPath) # Proactively set ownership, then apply root ACL if ($isDirectory) { [System.IO.Directory]::SetAccessControl($itemPath, $dirOwnerAcl) [System.IO.Directory]::SetAccessControl($itemPath, $rootAcl) } else { [System.IO.File]::SetAccessControl($itemPath, $fileOwnerAcl) [System.IO.File]::SetAccessControl($itemPath, $rootAcl) } } catch [System.UnauthorizedAccessException], [System.IO.IOException] { Write-Warning "Failed to process $itemPath : $_" continue } } } END { # Disable privileges once "SeRestorePrivilege","SeBackupPrivilege","SeTakeOwnershipPrivilege" | Adjust-Privilege -Disable } } # Execute with long-path root Restore-Inheritance -RootPath $longRoot
Extra Tips for Maximum Speed
- Run locally on the server: Avoid SMB latency by executing the script directly on the NAS/file server instead of over the network.
- Temporarily disable real-time AV: Antivirus scanning adds massive overhead when modifying thousands of files—just re-enable it afterward.
- Consider
icaclsfor ultimate speed: If you don't strictly need PowerShell,icacls $longRoot /reset /T /Cis natively optimized for this task, handles long paths, and automates ownership changes with admin privileges.
内容的提问来源于stack exchange,提问作者Gavin Greenhorn

