<# .SYNOPSIS Tests a folder full of mixed archives and, optionally, converts them all to one format. .DESCRIPTION Runs 7-Zip's integrity test on every archive it finds (.zip, .7z, .rar, .tar by default). With -TestOnly, that's all it does. Otherwise each archive that passes is extracted to a private temp folder, repacked as ZIP, 7z or TAR, tested again, and moved into place. Archives that fail the test are never converted or deleted. Supports -WhatIf. .PARAMETER Path Archive files, or folders to search. Accepts pipeline input. .PARAMETER To Target format: zip, 7z or tar. Default 7z. .PARAMETER Include File extensions to treat as archives. Default .zip, .7z, .rar, .tar. .PARAMETER TestOnly Only test the archives and report. Nothing is written or deleted. .PARAMETER Recurse Search subfolders when Path is a folder. .PARAMETER Destination Folder for converted archives. Defaults to the same folder as each source. .PARAMETER CompressionLevel 7-Zip -mx level for zip and 7z output. Default 7. Ignored for tar. .PARAMETER RemoveSource Delete each source archive after its replacement has been built and tested. .PARAMETER Force Overwrite an existing file at the destination. .PARAMETER SevenZipPath Full path to 7z.exe. If omitted, the script checks PATH, then the usual Program Files folders. .EXAMPLE .\Convert-Archive.ps1 -Path E:\Archive -Recurse -TestOnly .EXAMPLE .\Convert-Archive.ps1 -Path E:\Archive -Recurse -To 7z -RemoveSource -WhatIf #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName')] [string[]]$Path = '.', [ValidateSet('zip', '7z', 'tar')][string]$To = '7z', [string[]]$Include = @('.zip', '.7z', '.rar', '.tar'), [switch]$TestOnly, [switch]$Recurse, [string]$Destination, [ValidateSet(1, 3, 5, 7, 9)][int]$CompressionLevel = 7, [switch]$RemoveSource, [switch]$Force, [string]$SevenZipPath ) begin { if (-not $SevenZipPath) { $SevenZipPath = @( Get-Command -Name 7z.exe, 7z -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source if ($env:ProgramFiles) { Join-Path $env:ProgramFiles '7-Zip\7z.exe' } if (${env:ProgramFiles(x86)}) { Join-Path ${env:ProgramFiles(x86)} '7-Zip\7z.exe' } ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1 } if (-not $SevenZipPath -or -not (Test-Path -LiteralPath $SevenZipPath)) { throw '7-Zip not found. Install it from 7-zip.org or pass -SevenZipPath.' } Write-Verbose "Using 7-Zip at $SevenZipPath" function Invoke-SevenZip([string[]]$Arguments) { $out = & $SevenZipPath @Arguments 2>&1 if ($LASTEXITCODE -ge 2) { $errText = $out | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } | ForEach-Object { "$_".Trim() } | Where-Object { $_ -match '\w' -and $_ -notmatch 'RemoteException|^ERRORS:$' -and $_ -ne $Arguments[-1] } throw "7-Zip exit code ${LASTEXITCODE}: $(($errText | Select-Object -Unique) -join '; ')" } if ($LASTEXITCODE -eq 1) { Write-Warning "7-Zip reported warnings for $($Arguments[-1])" } } $formatArgs = switch ($To) { 'zip' { @('-tzip', '-mm=Deflate', "-mx=$CompressionLevel", '-mcu=on') } '7z' { @('-t7z', "-mx=$CompressionLevel") } 'tar' { @('-ttar') } } } process { $archives = foreach ($item in $Path) { if (Test-Path -LiteralPath $item -PathType Container) { Get-ChildItem -LiteralPath $item -File -Recurse:$Recurse | Where-Object { $Include -contains $_.Extension.ToLower() } } else { Get-Item -LiteralPath $item } } foreach ($archive in $archives) { $outDir = if ($Destination) { $Destination } else { $archive.DirectoryName } $target = Join-Path $outDir "$($archive.BaseName).$To" $result = [ordered]@{ Name = $archive.Name; SizeKB = [math]::Round($archive.Length / 1KB); Test = $null; Status = $null; Target = $null } try { Invoke-SevenZip @('t', '-bd', $archive.FullName) $result.Test = 'Passed' } catch { $result.Test = 'Failed' $result.Status = $_.Exception.Message Write-Warning "$($archive.Name) failed its integrity test and will be left alone." [pscustomobject]$result continue } if ($TestOnly) { $result.Status = 'Tested'; [pscustomobject]$result; continue } if ($archive.Extension -eq ".$To") { $result.Status = 'AlreadyTarget'; [pscustomobject]$result; continue } if ((Test-Path -LiteralPath $target) -and -not $Force) { $result.Status = 'SkippedExists'; [pscustomobject]$result; continue } if (-not $PSCmdlet.ShouldProcess($archive.FullName, "Convert to $target")) { continue } $work = Join-Path ([IO.Path]::GetTempPath()) ('convert-' + [guid]::NewGuid().ToString('N')) try { $files = Join-Path $work 'files' $null = New-Item -ItemType Directory -Path $files -Force Invoke-SevenZip @('x', '-y', '-bd', "-o$files", $archive.FullName) if (-not (Get-ChildItem -LiteralPath $files -Force)) { throw 'Archive is empty.' } $tempOut = Join-Path $work "out.$To" Invoke-SevenZip (@('a') + $formatArgs + @('-y', '-bd', $tempOut, (Join-Path $files '*'))) Invoke-SevenZip @('t', '-bd', $tempOut) if (-not (Test-Path -LiteralPath $outDir)) { $null = New-Item -ItemType Directory -Path $outDir } Move-Item -LiteralPath $tempOut -Destination $target -Force -WhatIf:$false $result.Target = $target $result.Status = 'Converted' if ($RemoveSource -and $PSCmdlet.ShouldProcess($archive.FullName, 'Delete original archive')) { Remove-Item -LiteralPath $archive.FullName -Force $result.Status = 'ConvertedRemovedSource' } } catch { $result.Status = "Failed: $($_.Exception.Message)" Write-Warning "$($archive.Name): $($_.Exception.Message)" } finally { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue -WhatIf:$false } [pscustomobject]$result } }