SCRIPT LIBRARY · POWERSHELL
Test and Convert a Folder of Mixed Archives with 7-Zip
Integrity-test every ZIP, 7z, RAR and TAR in a folder, then optionally repack the good ones into a single format. Broken archives never get touched.
- What it does
- Runs 7-Zip's integrity test on every archive it finds. With -TestOnly that's the whole job; otherwise each archive that passes gets repacked as ZIP, 7z or TAR, tested again, and moved into place.
- Requires
- PowerShell 7+ or Windows PowerShell 5.1
- 7-Zip (7z.exe, the full version, not 7za)
- Permissions
- Read access to the archives, and write access to wherever the new ones go. No admin rights.
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked and run against real ZIP, 7z, TAR and deliberately broken archives with 7-Zip 23.01 in PowerShell 7.4, including -WhatIf and -RemoveSource
Part 1 of the thread Archive conversion workshop
Every file share eventually grows a folder like this. Some ZIPs, a few .7z files somebody's tool spat out, a RAR from a vendor, a TAR that came off a Linux box, and at least one archive that's been quietly corrupt since 2019. Nobody knows which one.
This is the first part of a small series on moving archives between formats with 7-Zip. This one's the general tool: point it at a folder, and it tests every archive first. If you only want to know what's broken, stop there with -TestOnly. If you want everything in one format, it repacks the ones that pass and leaves the failures exactly where they were. The next two parts go deeper on the two conversions people actually ask for: 7z to ZIP for sharing, and ZIP to 7z for saving space.
The old version of this post converted .7z to .zip, used Set-Location to hop in and out of folders, and deleted the originals without checking anything. This one builds everything in a temp folder, tests the result, and only deletes when you ask.
<#
.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
}
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-Path | string[] | . | Archive files, or folders to search. Takes pipeline input, so Get-ChildItem works too. |
-To | string | 7z | The format to convert to. zip, 7z or tar. |
-Include | string[] | .zip, .7z, .rar, .tar | Which extensions count as archives when searching a folder. |
-TestOnly | switch | — | Test and report, nothing else. Nothing is written or deleted. |
-Recurse | switch | — | Look in subfolders too. |
-Destination | string | — | Put converted archives here instead of next to the originals. |
-CompressionLevel | int | 7 | The 7-Zip -mx level for ZIP and 7z output, 1 to 9. Ignored for TAR. |
-RemoveSource | switch | — | Delete each original once its replacement has been built and tested. Honors -WhatIf. |
-Force | switch | — | Overwrite a file that already exists at the target path. |
-SevenZipPath | string | — | Full path to 7z.exe. Leave it off and the script checks PATH, then Program Files and Program Files (x86). |
Run it
Find out what's broken in an old archive share, without changing anything.
.\Convert-Archive.ps1 -Path E:\Archive -Recurse -TestOnly | Where-Object Test -eq 'Failed'Preview a full conversion to 7z, including the deletes.
.\Convert-Archive.ps1 -Path E:\Archive -Recurse -To 7z -RemoveSource -WhatIfNormalize a vendor drop folder to plain ZIP and keep the originals.
.\Convert-Archive.ps1 -Path .\VendorDrops -To zip -Destination .\Normalized7-Zip installed somewhere unusual, like a portable copy on a tools share.
.\Convert-Archive.ps1 -Path .\in -TestOnly -SevenZipPath '\\fs01\Tools\7-Zip\7z.exe'What you'll see
WARNING: budget-2019.zip failed its integrity test and will be left alone.
Name SizeKB Test Status Target
---- ------ ---- ------ ------
budget-2019.zip 412 Failed 7-Zip exit code 2: ...
site-photos.zip 88210 Passed ConvertedRemovedSource E:\Archive\site-photos.7z
drivers-pc0142.rar 20544 Passed ConvertedRemovedSource E:\Archive\drivers-pc0142.7z
logs-web01.tar 61377 Passed ConvertedRemovedSource E:\Archive\logs-web01.7z
old-backup.7z 9921 Passed AlreadyTarget
How it works
- Find 7-Zip. It uses
-SevenZipPathif you gave it one, otherwise whatever7zis on your PATH, otherwise the standard install folders. If none of those exist it stops right there instead of failing on every file. - Test first, always. Every archive gets
7z tbefore anything else happens. A failure is reported and the file is skipped. Nothing that fails a test is ever converted or deleted. - Stop there, or keep going. With
-TestOnlyyou get the report and nothing else. Archives already in the target format are markedAlreadyTargetand skipped. - Rebuild in a private temp folder. Each archive is extracted into its own GUID-named folder under
%TEMP%, repacked, and tested again. Only then is the new file moved to its real name, so you never end up with a half-written archive sitting next to the original. - Delete only when asked.
-RemoveSourceremoves the original after the new file checks out, and it goes throughShouldProcess, so-WhatIfshows you every delete it would make. The temp folder is cleaned up either way.
Take it further
- Run the test on a schedule.
-TestOnlyagainst a backup or archive share once a month, exported to CSV, catches bit rot while you still have another copy. - Add .cab or .iso to -Include. 7-Zip reads both, so they'll test and convert like anything else.
- Pipe it.
Get-ChildItem -Recurse *.zip | Where-Object Length -gt 1GB | .\Convert-Archive.ps1 -To 7zconverts only the big ones.
Things that'll trip you up
- Encrypted archives stop and ask. If an archive has a password, 7-Zip prompts for it during the test. At an interactive prompt you'll see that; in a scheduled task it just fails that file. The 7z-to-ZIP script in part two takes a -Password for exactly this.
- .tar.gz is two layers. 7-Zip treats a .gz or .tgz as a compressed wrapper around a TAR, so extracting it gives you the .tar, not the files. That's why .gz isn't in the default -Include list. Unwrap those first, then point the script at the .tar files.
- You need temp space for the biggest archive, twice. Each archive is fully extracted to your temp folder and then repacked there before it moves. A 40 GB archive needs something like 80 GB free on the drive that holds %TEMP%.
- Converting from RAR is fine, converting to RAR isn't. 7-Zip can read RAR (including RAR5) but can't create it, which is why RAR isn't a -To option.
- Exit code 1 is a warning, not a failure. 7-Zip returns 1 for things like a file it couldn't read during an add. The script shows a warning and carries on; 2 and above count as failures.
- Convert 7z Archives to ZIP So Anyone Can Open ThemScript Library
- Convert ZIP Files to 7z, but Only When It Actually Saves SpaceScript Library
- Zip Up Folders Nobody Has Touched in MonthsScript Library