PwSh.Fw.Archive.psm1

Add-Type -Assembly System.IO.Compression.FileSystem

<#
.SYNOPSIS
List the files contained in an archive
 
.DESCRIPTION
Long description
 
.PARAMETER Path
Full path to the archive file
 
.PARAMETER PassThru
If specified, Get-ArchiveContent will return whole archive file object.
If not specified, Get-ArchiveContent will return file list
 
.EXAMPLE
archive.zip | Get-ArchiveContent
 
.NOTES
Inspired by @url https://stackoverflow.com/questions/43715949/powershell-extract-specific-files-folders-from-a-zipped-archive
#>

function Get-ArchiveContent {
    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$Path,
        [switch]$PassThru
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        if (!(Test-FileExist $Path)) {
            Write-Fatal "Archive '$Path' not found."
        }
        # $archive = Get-Item -Path $Path

        #extract list entries for dir myzipdir/c/ into myzipdir.zip
        $zip = [IO.Compression.ZipFile]::OpenRead($Path)
        $entries = $zip.Entries
        #free object
        $zip.Dispose()

        if ($PassThru) {
            return $entries
        } else {
            return $entries.fullname
        }
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Expand a single from an archive
 
.DESCRIPTION
Extract only one specified file from an archive.
 
.PARAMETER Path
Path to the archive file
 
.PARAMETER DestinationPath
Destination path where to extract files
 
.PARAMETER Files
Exact filename to extract. The path must be relative to the root of the archive.
Wildcard are not allowed
 
.PARAMETER Force
If specified, erase existing files
 
.PARAMETER PassThru
If specified, return filesystem object of files extracted.
If not specified, return a list of path/files extracted
 
.EXAMPLE
"archive.zip" | Expand-SingleFileFromArchive -File "README.md"
 
.EXAMPLE
"archive.zip" | Expand-SingleFileFromArchive -File "src/path/to/file.c"
 
This example extract the file 'file.c' from the "archive.zip" file, located in the "src/path/to" folder inside the archive.
 
.NOTES
Inspired by @url https://stackoverflow.com/questions/43715949/powershell-extract-specific-files-folders-from-a-zipped-archive
#>

function Expand-SingleFileFromArchive {
    [CmdletBinding()]
    [OutputType([Boolean], [System.IO.FileInfo])]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$Path,
        [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$DestinationPath,
        [Parameter(Mandatory = $true, ValueFromPipeLine = $false)][string]$File,
        [switch]$Force,
        [switch]$PassThru
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        $rc = $false
        if (!(Test-FileExist $Path)) {
            Write-Fatal "Archive '$Path' not found."
        }
        $archive = Get-Item -Path $Path
        if ([string]::IsNullOrEmpty($DestinationPath)) {
            $DestinationPath = "$($archive.Directory)/$($archive.BaseName)"
        }
        #create dir for result of extraction
        $FileName = Split-Path $File -Leaf
        if ($Force) {
            if (Test-FileExist "$DestinationPath/$FileName") { Remove-Item -Path "$DestinationPath/$FileName" -Confirm:$false -Force:$Force }
            $null = New-Item -ItemType Directory -Path "$DestinationPath" -Force:$Force -ErrorAction SilentlyContinue
        }

        #extract list entries for dir myzipdir/c/ into myzipdir.zip
        try {
            $zip = [IO.Compression.ZipFile]::OpenRead($Path)
            $entry = $zip.GetEntry($File)
            if ($null -ne $entry) {
                #extraction
                [IO.Compression.ZipFileExtensions]::ExtractToFile( $entry, "$DestinationPath/$($entry.Name)", $Force)
                $rc = $?
            } else {
                eerror "File '$File' not found in archive '$($archive.Name)'"
                eerror "Try to Get-ArchiveContent to see what's inside."
                eerror "Remember, -File must be the full path to file from the archive point of view."
                $rc = $false
            }
        } catch {
            eerror $_
            $rc = $false
        } finally {
            #free object
            $zip.Dispose()
        }

        if ($PassThru) {
            if ($rc) { return "$DestinationPath/$($entry.Name)" } else { return $rc }
        } else {
            return $rc
        }
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Expand a CAB archive
 
.DESCRIPTION
Extract all files from a Microsoft Cabinet (.cab) archive.
This function relies on platform tools, because no GPL-compatible
managed implementation exists:
- Windows: built-in Expand cmdlet (Microsoft.PowerShell.Archive)
- Linux/macOS: cabextract, invoked via PwSh.Fw.Core Execute-Command
 
.PARAMETER Path
Full path to the .cab archive file
 
.PARAMETER DestinationPath
Destination folder where to extract files.
If not specified, files are extracted to a folder next to the
archive, named after the archive (without extension).
 
.PARAMETER Force
If specified, erase existing files before extraction.
Note: on Linux/macOS, cabextract overwrites existing files anyway.
 
.PARAMETER PassThru
If specified, return the list of extracted file paths.
If not specified, return $true on success, $false on failure.
 
.EXAMPLE
"archive.cab" | Expand-CabFile
 
.EXAMPLE
"archive.cab" | Expand-CabFile -DestinationPath /tmp/out -Force
 
.NOTES
CAB support is delegated to platform tools (Expand on Windows,
cabextract elsewhere) since managed CAB libraries are not
GPL-compatible (WixToolset is MS-RL / fee-EULA licensed).
#>

function Expand-CabFile {
    [CmdletBinding()]
    [OutputType([Boolean], [String])]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][string]$Path,
        [Parameter(Mandatory = $false, ValueFromPipeLine = $false)][string]$DestinationPath,
        [switch]$Force,
        [switch]$PassThru
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        $rc = $false
        if (!(Test-FileExist $Path)) {
            Write-Fatal "Archive '$Path' not found."
        }
        $archive = Get-Item -Path $Path
        if ([string]::IsNullOrEmpty($DestinationPath)) {
            $DestinationPath = "$($archive.Directory)/$($archive.BaseName)"
        }

        if ($IsWindows) {
            # built-in cmdlet from Microsoft.PowerShell.Archive
            if ($Force) {
                # Expand has no overwrite option: clean destination first
                Get-ChildItem -Path $DestinationPath -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
            }
            $null = New-Item -ItemType Directory -Path $DestinationPath -Force -ErrorAction SilentlyContinue
            try {
                Expand $Path -F:* $DestinationPath -ErrorAction Stop
                $rc = $true
            } catch {
                eerror $_
                $rc = $false
            }
        } else {
            if (!(Get-Command cabextract -ErrorAction SilentlyContinue)) {
                Write-Fatal "CAB extractor 'cabextract' not found. Install it (e.g. 'apt install cabextract' or 'brew install cabextract')."
            }
            $null = New-Item -ItemType Directory -Path $DestinationPath -Force -ErrorAction SilentlyContinue
            $toolArgs = "-q -d '$DestinationPath'"
            if ($Force) { $toolArgs += " -f" }
            $toolArgs += " '$Path'"
            $rc = Execute-Command -exe "cabextract" -args $toolArgs -AsBool
            if (!$rc) {
                eerror "cabextract failed on archive '$Path'."
            }
        }

        if ($PassThru) {
            if ($rc) { return (Get-ChildItem -Path $DestinationPath -Recurse -File -Force).FullName } else { return $rc }
        } else {
            return $rc
        }
    }

    End {
        Write-LeaveFunction
    }
}