<# .SYNOPSIS Repacks ZIP archives as 7z and tells you how much space it actually saved. .DESCRIPTION Extracts each ZIP to a private temp folder, builds a .7z from the contents, tests the new archive with 7-Zip, and only then moves it into place. If the 7z isn't smaller than the ZIP, it's thrown away and the ZIP is kept (unless you say otherwise). The original ZIP is only deleted when you ask for it with -RemoveSource. Supports -WhatIf. .PARAMETER Path ZIP files, or folders to search for ZIP files. Accepts pipeline input. .PARAMETER Recurse Search subfolders when Path is a folder. .PARAMETER Destination Folder for the new .7z files. Defaults to the same folder as each ZIP. .PARAMETER CompressionLevel 7-Zip -mx level: 1 (fastest) to 9 (smallest). Default 9. .PARAMETER KeepLarger Keep the .7z even when it's the same size or bigger than the ZIP. .PARAMETER RemoveSource Delete each ZIP after its .7z has been built and tested. .PARAMETER Force Overwrite a .7z that already exists at the destination. .PARAMETER SevenZipPath Full path to 7z.exe. If omitted, the script checks PATH, then the usual Program Files folders. .EXAMPLE .\Convert-ZipTo7z.ps1 -Path D:\Downloads -WhatIf .EXAMPLE .\Convert-ZipTo7z.ps1 -Path D:\Archive -Recurse -RemoveSource #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName')] [string[]]$Path = '.', [switch]$Recurse, [string]$Destination, [ValidateSet(1, 3, 5, 7, 9)][int]$CompressionLevel = 9, [switch]$KeepLarger, [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])" } } } process { $zips = foreach ($item in $Path) { if (Test-Path -LiteralPath $item -PathType Container) { Get-ChildItem -LiteralPath $item -Filter *.zip -File -Recurse:$Recurse } else { Get-Item -LiteralPath $item } } foreach ($zip in $zips) { $outDir = if ($Destination) { $Destination } else { $zip.DirectoryName } $target = Join-Path $outDir ($zip.BaseName + '.7z') $result = [ordered]@{ Name = $zip.Name; ZipKB = [math]::Round($zip.Length / 1KB); SevenZipKB = $null; SavedPercent = $null; Status = $null; Target = $target } if ((Test-Path -LiteralPath $target) -and -not $Force) { $result.Status = 'SkippedExists' [pscustomobject]$result continue } if (-not $PSCmdlet.ShouldProcess($zip.FullName, "Repack as $target")) { continue } $work = Join-Path ([IO.Path]::GetTempPath()) ('zip2-7z-' + [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", $zip.FullName) if (-not (Get-ChildItem -LiteralPath $files -Force)) { throw 'ZIP is empty.' } $temp7z = Join-Path $work 'out.7z' Invoke-SevenZip @('a', '-t7z', "-mx=$CompressionLevel", '-y', '-bd', $temp7z, (Join-Path $files '*')) Invoke-SevenZip @('t', '-bd', $temp7z) $newSize = (Get-Item -LiteralPath $temp7z).Length $result.SevenZipKB = [math]::Round($newSize / 1KB) $result.SavedPercent = if ($zip.Length) { [math]::Round((1 - $newSize / $zip.Length) * 100, 1) } else { 0 } if ($newSize -ge $zip.Length -and -not $KeepLarger) { $result.Status = 'NoGainKeptZip' } else { if (-not (Test-Path -LiteralPath $outDir)) { $null = New-Item -ItemType Directory -Path $outDir } Move-Item -LiteralPath $temp7z -Destination $target -Force -WhatIf:$false $result.Status = 'Converted' if ($RemoveSource -and $PSCmdlet.ShouldProcess($zip.FullName, 'Delete original ZIP')) { Remove-Item -LiteralPath $zip.FullName -Force $result.Status = 'ConvertedRemovedZip' } } } catch { $result.Status = "Failed: $($_.Exception.Message)" Write-Warning "$($zip.Name): $($_.Exception.Message)" } finally { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue -WhatIf:$false } [pscustomobject]$result } }