PowerShell 7目录扁平化函数文件名冲突问题及修复方案
PowerShell 目录扁平化函数开发与问题修复
预期功能
在PowerShell 7环境下开发的目录扁平化函数,预期实现以下能力:
- 根据是否传入目标路径参数,将所有内容复制或移动到临时目录;
- 对文件名重复的文件添加_XX格式的数字后缀重命名,后缀的补零位数由参数控制;
- 将处理完成的所有文件移动到原目录根路径,或是用户指定的目标目录。
原始核心代码
function Merge-FlattenDirectory { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [ValidateScript({ if (!(Test-Path -LiteralPath $_)) { throw [System.ArgumentException] "Path does not exist." } if ((Test-IsSensitiveWindowsPath -Path $_ -Strict).IsSensitive) { throw [System.ArgumentException] "Path supplied is a protected OS directory." } return $true })] [Alias("source", "input", "i")] [string] $SourcePath, [Parameter(Mandatory = $false, Position = 1, ValueFromPipelineByPropertyName)] [Alias("destination", "dest", "output", "o")] [string] $DestinationPath = $null, [Parameter(Mandatory=$false)] [Switch] $Force, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)] [ValidateSet(1, 2, 3, 4, 5)] [int32] $DuplicatePadding = 2 ) begin { # Trim trailing backslashes and initialize a new temporary directory. $SourcePath = $SourcePath.TrimEnd('\') $DestinationPath = $DestinationPath.TrimEnd('\') $TempPath = (New-TempDirectory).FullName New-Item -ItemType Directory -Force -Path $TempPath # Escape $SourcePath so we can use wildcards. $Source = [WildcardPattern]::Escape($SourcePath) # If there is no $DestinationPath supplied, we flatten only the SourcePath. # Thus, set DestinationPath to be the same as the SourcePath. if (!$DestinationPath) { $DestinationPath = $SourcePath # Since there is no destination supplied, we move everything to a temporary # directory for further processing. Move-Item -Path $Source'\*' -Destination $TempPath -Force }else{ # We need to perform some parameter validation on DestinationPath: # Make sure the passed Destination is not a file if(Test-Path -LiteralPath $DestinationPath -PathType Leaf){ throw [System.IO.IOException] "Please provide a valid directory, not a file." } # Make sure the passed Destination is a validly formed Windows path. if(!(Confirm-ValidWindowsPath -Path $DestinationPath -Container)){ throw [System.IO.IOException] "Invalid Destination Path. Please provide a valid directory." } # Make sure the passed Destination is not in a protected or sensitive OS location. if((Test-IsSensitiveWindowsPath -Path $DestinationPath -Strict).IsSensitive){ throw [System.IO.IOException] "The destination path is, or resides in a protected operating system directory." } # Since a destination was supplied, we copy everything to a new temp directory # instead of moving everything. We want the source directory to remain untouched. # Robocopy seems to be the most performant here. # Robocopy on Large Dataset: ~789ms - ~810ms # Copy-Item on Large Dataset: ~1203ms - ~1280ms # # Copy-Item -Path $Source'\*' -Destination $TempPath -Force -Recurse Robocopy $Source $TempPath /COPYALL /B /E /R:0 /W:0 /NFL /NDL /NC /NS /NP /MT:48 # Create the destination directory now, ready for population in the process block. New-Item -ItemType Directory -Force -Path $DestinationPath } # Grab all files as an Array of FileInfo Objects $AllFiles = [IO.DirectoryInfo]::new($TempPath).GetFiles('*', 'AllDirectories') # Initialize hashtable to store duplicate files $Duplicates = @{} } process { ## # $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # # Iterate over all files foreach ($File in $AllFiles) { # If our $Duplicates hashtable already contains the current filename, we have a duplicate. if ($Duplicates.Contains($File.Name)) { # Rename the duplicate file by appending a numerical index to the end of the file. $PathTemp = Get-ItemProperty -LiteralPath $File $RenamedFile = Rename-Item -LiteralPath $PathTemp.PSPath -PassThru -NewName ('{0}_{1}{2}' -f @( $File.BaseName $Duplicates[$File.Name].ToString().PadLeft($DuplicatePadding, '0') $File.Extension )) # Increment the duplicate counter and pass $File down to be moved. $Duplicates[$File.Name]++ $File = $RenamedFile } else { # No duplicates were detected. Add a value of 1 to the duplicates # hashtable to represent the current file. Pass $File down to be moved. $PathTemp = Get-ItemProperty -LiteralPath $File $Duplicates[$File.Name] = 1 $File = $PathTemp } # If Force is specified, we don't have to worry about duplicate files, # as the operation will overwrite every file with a duplicate filename if($Force){ # Move the file to its appropriate destination. (Force) Move-Item -LiteralPath $File -Destination $DestinationPath -Force } else { try { # Move the file to its appropriate destination. (Non-Force) Move-Item -LiteralPath $File -Destination $DestinationPath -ErrorAction Stop } catch { # Warn the user that files were skipped because of duplicate filenames. Write-Warning "File already exists in the destination folder. Skipping this file." } } # Return each file to the pipeline. # $File } # $Stopwatch.Stop() # Write-Host "`$Stopwatch.Elapsed: " $Stopwatch.Elapsed -ForegroundColor Green # Write-Host "`$Stopwatch.ElapsedMilliseconds:" $Stopwatch.ElapsedMilliseconds -ForegroundColor Green # Write-Host "`$Stopwatch.ElapsedTicks: " $Stopwatch.ElapsedTicks -ForegroundColor Green } end { } } # Merge-FlattenDirectory "C:\Users\username\Desktop\Testing\Test" "C:\Users\username\Desktop\Testing\TestFlat" -Force
问题与修复
故障现象
该函数大部分场景下运行正常,但存在未预料到的文件名冲突漏洞。
触发问题的目录结构如下(待扁平化的根目录为C:\Users\username\Desktop\Testing\Test):
Directory: C:\Users\username\Desktop\Testing\Test Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 11/4/2021 10:03 PM 1552565 1088_p_01.jpg -a--- 11/4/2021 10:03 PM 1552565 1088_p_02.jpg -a--- 11/4/2021 10:03 PM 1552565 1088_p_03.jpg Directory: C:\Users\username\Desktop\Testing\Test\Folder Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 11/4/2021 10:03 PM 1552565 1088_p_03.jpg -a--- 11/4/2021 10:03 PM 1552565 1088_p.jpg Directory: C:\Users\username\Desktop\Testing\Test\Testing Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 11/4/2021 10:03 PM 1552565 1088_p_01.jpg -a--- 11/4/2021 10:03 PM 1552565 1088_p.jpg
调用函数对上述目录执行扁平化操作时,目标目录最终仅生成6个文件而非预期的7个,第二个1088_p.jpg丢失,经排查该文件遗留在临时目录C:\Users\username\AppData\Local\Temp\DdtElMvSoXbJf\Testing\1088_p.jpg中。
修复逻辑
对函数进行重构以覆盖该边界场景,解决方案为:新增一层重复命名校验逻辑,将文件重命名操作后移,生成新名称后先校验是否和已存在的文件名冲突,若冲突则继续追加数字序号直至无重复后再执行重命名。
修改后核心代码
# Iterate over all files foreach ($File in $AllFiles) { # If our $Duplicates hashtable already contains the current filename, we have a duplicate. if ($Duplicates.Contains($File.Name)) { # Create a new name for the file by appending a numerical index to the end of the filename. $PathTemp = Get-ItemProperty -LiteralPath $File $NewName = ('{0}_{1}{2}' -f @( $File.BaseName $Duplicates[$File.Name].ToString().PadLeft($DuplicatePadding, '0') $File.Extension )) # Check if our new name collides with any other filenames in $Duplicates. If so, create # another new name by appending an additional numeric index to the end of the filename. $DuplicateCount = 1 while ($Duplicates[$NewName]) { $NewName = ('{0}_{1}{2}' -f @( [System.IO.Path]::GetFileNameWithoutExtension($NewName) $DuplicateCount.ToString().PadLeft($DuplicatePadding, '0') [System.IO.Path]::GetExtension($NewName) )) Write-Warning $DuplicateCount.ToString().PadLeft($DuplicatePadding, '0') $DuplicateCount++ # If we're at a depth of 8, throw. Something is obviously wrong. if ($DuplicateCount -ge 8) { throw [System.Exception] "Duplicate count reached limit." break } } # Finally, rename the file with our new name. $RenamedFile = Rename-Item -LiteralPath $PathTemp.PSPath -PassThru -NewName $NewName # Increment the duplicate counters and pass $File down to be moved. $Duplicates[$File.Name]++ $Duplicates[$NewName]++ $File = $RenamedFile } else { # No duplicates were detected. Add a value of 1 to the duplicates # hashtable to represent the current file. Pass $File down to be moved. $PathTemp = Get-ItemProperty -LiteralPath $File $Duplicates[$File.Name] = 1 $File = $PathTemp } # If Force is specified, we don't have to worry about duplicate files, # as the operation will overwrite every file with a duplicate filename if($Force){ # Move the file to its appropriate destination. (Force) Move-Item -LiteralPath $File -Destination $DestinationPath -Force } else { try { # Move the file to its appropriate destination. (Non-Force) Move-Item -LiteralPath $File -Destination $DestinationPath -ErrorAction Stop } catch { # Warn the user that files were skipped because of duplicate filenames. Write-Warning "File already exists in the destination folder. Skipping this file." } } # Return each file to the pipeline. $File }
内容的提问来源于stack exchange,提问作者fmotion1
相关产品推荐
相关产品推荐

