Public/Compare-DirectoryContentParallel.ps1
|
function Compare-DirectoryContentParallel { <# .SYNOPSIS Compares files between two directories using multithreaded content hashing. .DESCRIPTION Scans a Source directory (or specific file list) and compares against a Destination. DEFAULT OUTPUT: Displays "FileName", "Status", "SourceLWT", and "DestLWT". HIDDEN OUTPUT: Use "| Select-Object *" or "| Format-List" to see hidden properties: - SourcePath - DestPath .PARAMETER Source The root folder of the source files. .PARAMETER Destination The root folder of the destination files. .PARAMETER FileList Optional. Array of relative paths. If provided, only these files are checked. .PARAMETER ShowAll (Alias: IncludeMatch). If specified, the output includes files that match. Without this, only differences/errors are returned. .PARAMETER ThrottleLimit Number of parallel threads. Default: 16. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Source, [Parameter(Mandatory = $true)] [string]$Destination, [string[]]$FileList, [ValidateSet("MD5", "SHA1", "SHA256")] [string]$Algorithm = "MD5", [Alias('IncludeMatch')] [switch]$ShowAll, [int]$ThrottleLimit = 16 ) if ($PSVersionTable.PSVersion.Major -lt 7) { Throw "This function requires PowerShell Core 7.0 or newer." } $Source = $Source.TrimEnd('\') $Destination = $Destination.TrimEnd('\') # --- Build the list of files to process --- if ($FileList) { Write-Verbose "File list provided. Processing $($FileList.Count) specific files..." $files = foreach ($path in $FileList) { $cleanPath = $path.TrimStart('\').TrimStart('/') $fullSourcePath = Join-Path -Path $Source -ChildPath $cleanPath if (Test-Path $fullSourcePath -PathType Leaf) { Get-Item $fullSourcePath } else { Write-Warning "Skipping: File not found in source ($cleanPath)" } } } else { Write-Verbose "Scanning entire source directory recursively..." $files = Get-ChildItem -Path $Source -File -Recurse } if (-not $files) { Write-Warning "No valid files found to process." return } Write-Verbose "Starting comparison of $($files.Count) files using $ThrottleLimit threads..." # --- Parallel Processing --- $files | ForEach-Object -Parallel { $file = $_ # Pass variables into parallel scope $srcRoot = $using:Source $dstRoot = $using:Destination $algo = $using:Algorithm $showAll = $using:ShowAll $relativePath = $file.FullName.Substring($srcRoot.Length) $destPath = "$dstRoot$relativePath" # Initialize the properties dictionary $props = [ordered]@{ FileName = $relativePath Status = "Unknown" SourcePath = $file.FullName DestPath = $destPath SourceLWT = $file.LastWriteTime DestLWT = $null } # 1. Check if file exists in destination if (-not (Test-Path $destPath)) { $props.Status = "MissingInDest" } else { # Get Destination Item for Metadata $destFile = Get-Item $destPath $props.DestLWT = $destFile.LastWriteTime # 2. Calculate Hashes $srcHash = (Get-FileHash -LiteralPath $file.FullName -Algorithm $algo).Hash $dstHash = (Get-FileHash -LiteralPath $destPath -Algorithm $algo).Hash # 3. Compare Content if ($srcHash -ne $dstHash) { $props.Status = "ContentMismatch" # Check for "Silent" mismatch (Same Size/Time, different content) if ($file.Length -eq $destFile.Length -and $file.LastWriteTime.ToString() -eq $destFile.LastWriteTime.ToString()) { $props.Status = "SilentCorruption" } } elseif ($showAll) { $props.Status = "Match" } } # --- Output Logic --- if ($props.Status -ne "Unknown") { # Create the object $obj = [PSCustomObject]$props # define DefaultDisplayPropertySet (Included LWT properties here) $defaultDisplayProps = @('FileName', 'Status', 'SourceLWT', 'DestLWT') $propSet = [System.Management.Automation.PSPropertySet]::new('DefaultDisplayPropertySet', [string[]]$defaultDisplayProps) $memberSet = [System.Management.Automation.PSMemberSet]::new('PSStandardMembers', [System.Management.Automation.PSMemberInfo[]]@($propSet)) # Add the member set to the object $obj.PSObject.Members.Add($memberSet) return $obj } } -ThrottleLimit $ThrottleLimit } |