Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

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.

AT A GLANCEConvert-Archive.ps1
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

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.

Convert-Archive.ps1Download
<#
.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

ParameterTypeDefaultWhat it's for
-Pathstring[].Archive files, or folders to search. Takes pipeline input, so Get-ChildItem works too.
-Tostring7zThe format to convert to. zip, 7z or tar.
-Includestring[].zip, .7z, .rar, .tarWhich extensions count as archives when searching a folder.
-TestOnlyswitch—Test and report, nothing else. Nothing is written or deleted.
-Recurseswitch—Look in subfolders too.
-Destinationstring—Put converted archives here instead of next to the originals.
-CompressionLevelint7The 7-Zip -mx level for ZIP and 7z output, 1 to 9. Ignored for TAR.
-RemoveSourceswitch—Delete each original once its replacement has been built and tested. Honors -WhatIf.
-Forceswitch—Overwrite a file that already exists at the target path.
-SevenZipPathstring—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 -WhatIf

Normalize a vendor drop folder to plain ZIP and keep the originals.

.\Convert-Archive.ps1 -Path .\VendorDrops -To zip -Destination .\Normalized

7-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

Example outputvalues are illustrative
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

  1. Find 7-Zip. It uses -SevenZipPath if you gave it one, otherwise whatever 7z is on your PATH, otherwise the standard install folders. If none of those exist it stops right there instead of failing on every file.
  2. Test first, always. Every archive gets 7z t before anything else happens. A failure is reported and the file is skipped. Nothing that fails a test is ever converted or deleted.
  3. Stop there, or keep going. With -TestOnly you get the report and nothing else. Archives already in the target format are marked AlreadyTarget and skipped.
  4. 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.
  5. Delete only when asked. -RemoveSource removes the original after the new file checks out, and it goes through ShouldProcess, so -WhatIf shows you every delete it would make. The temp folder is cleaned up either way.

Take it further

  • Run the test on a schedule. -TestOnly against 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 7z converts 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.