<# .SYNOPSIS Repacks .7z archives as plain ZIP files that anything can open. .DESCRIPTION Extracts each .7z to a private temp folder, builds a Deflate ZIP with UTF-8 file names, tests the new ZIP with 7-Zip, and only then moves it into place. The .7z is kept unless you pass -RemoveSource. Works with password-protected .7z files if you supply the password. Supports -WhatIf. .PARAMETER Path .7z files, or folders to search for them. Accepts pipeline input. .PARAMETER Recurse Search subfolders when Path is a folder. .PARAMETER Destination Folder for the new ZIP files. Defaults to the same folder as each .7z. .PARAMETER Password Password for encrypted .7z files. The ZIP that comes out is NOT encrypted. .PARAMETER CompressionLevel Deflate level passed to 7-Zip as -mx: 1 (fastest) to 9 (smallest). Default 5. .PARAMETER RemoveSource Delete each .7z after its ZIP has been built and tested. .PARAMETER Force Overwrite a ZIP 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-7zToZip.ps1 -Path .\handoff -WhatIf .EXAMPLE .\Convert-7zToZip.ps1 -Path .\vendor-drop.7z -Password (Read-Host -AsSecureString 'Password') #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName')] [string[]]$Path = '.', [switch]$Recurse, [string]$Destination, [securestring]$Password, [ValidateSet(1, 3, 5, 7, 9)][int]$CompressionLevel = 5, [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" # Only unwrapped for the 7z.exe call itself. $passArg = if ($Password) { '-p' + [Net.NetworkCredential]::new('', $Password).Password } 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 { $archives = foreach ($item in $Path) { if (Test-Path -LiteralPath $item -PathType Container) { Get-ChildItem -LiteralPath $item -Filter *.7z -File -Recurse:$Recurse } else { Get-Item -LiteralPath $item } } foreach ($archive in $archives) { $outDir = if ($Destination) { $Destination } else { $archive.DirectoryName } $target = Join-Path $outDir ($archive.BaseName + '.zip') $result = [ordered]@{ Name = $archive.Name; SevenZipKB = [math]::Round($archive.Length / 1KB); ZipKB = $null; Files = $null; Status = $null; Target = $target } if ((Test-Path -LiteralPath $target) -and -not $Force) { $result.Status = 'SkippedExists' [pscustomobject]$result continue } if (-not $PSCmdlet.ShouldProcess($archive.FullName, "Repack as $target")) { continue } $work = Join-Path ([IO.Path]::GetTempPath()) ('7z2zip-' + [guid]::NewGuid().ToString('N')) try { $files = Join-Path $work 'files' $null = New-Item -ItemType Directory -Path $files -Force $extractArgs = @('x', '-y', '-bd', "-o$files") if ($passArg) { $extractArgs += $passArg } Invoke-SevenZip ($extractArgs + $archive.FullName) $result.Files = @(Get-ChildItem -LiteralPath $files -File -Recurse -Force).Count if (-not $result.Files) { throw 'Archive contains no files.' } # Deflate + UTF-8 names = the ZIP flavor that Explorer, macOS and most tools agree on. $tempZip = Join-Path $work 'out.zip' Invoke-SevenZip @('a', '-tzip', '-mm=Deflate', "-mx=$CompressionLevel", '-mcu=on', '-y', '-bd', $tempZip, (Join-Path $files '*')) Invoke-SevenZip @('t', '-bd', $tempZip) if (-not (Test-Path -LiteralPath $outDir)) { $null = New-Item -ItemType Directory -Path $outDir } $result.ZipKB = [math]::Round((Get-Item -LiteralPath $tempZip).Length / 1KB) Move-Item -LiteralPath $tempZip -Destination $target -Force -WhatIf:$false $result.Status = 'Converted' if ($RemoveSource -and $PSCmdlet.ShouldProcess($archive.FullName, 'Delete original .7z')) { Remove-Item -LiteralPath $archive.FullName -Force $result.Status = 'ConvertedRemoved7z' } } 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 } }