Search code examples
powershellfilezipextractsubdirectory

Powershell 2.0 extract certain files from zip (include subdirectories)


Apologies, this question is scattered on the internet but I have yet to find a satisfactory answer that uses only Powershell 2.0 (with .NET v3.5) - no external libraries or programs

I'm using the following code to extract log.txt from ZipFile.zip (no matter log.txt's location)

$Destination = (new-object -com shell.application).NameSpace('C:\ZipExtractDir')
$ZipFile = (new-object -com shell.application).NameSpace('C:\ZipFile.zip')

$Destination.CopyHere(($Zipfile.Items() | where-object {$_.Name -like '*log.txt'}), 1044)
  • Works if log.txt is in directory root \log.txt
  • Fails if log.txt is in a subdirectory \Subfolder\log.txt
  • Fails if referencing the literal (.zip) path {$_.Name -Like '*Subfolder\log.txt'} (both double & single quotes fail)
  • Have tried using -eq -like -contains '' "" $_.FullName

I'm quite certain that I'm filtering incorrectly - can anyone help with this code so that it will parse subdirectories as well?


Solution

  • Similar to what you have already done, you can set up the Shell.Application namespaces like this. Then you can copy the extracted directory to the destination path.

    $zipFilePath = "Zipfile.zip"
    $destinationPath = "C:\Users\Public\Downloads"
    
    $zipfile = (New-Object -Com Shell.Application).NameSpace($zipFilePath)
    $destination = (New-Object -Com Shell.Application).NameSpace($destinationPath)
    $destination.CopyHere($zipfile.Items())
    

    Then to list the log.txt files, we can contruct the full extracted path with Join-Path. This basically just appends the zip file name from System.IO.Path.GetFileNameWithoutExtension() to the destination path. Then just use Get-ChildItem to list the files recursively with the -Recurse and -Filter switches.

    $extractedPath = Join-Path -Path $destinationPath -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension($zipFilePath))
    Get-ChildItem -Path $extractedPath -Filter log.txt -Recurse
    

    And to test this for PowerShell 2.0 we can use -version 2 with powershell.exe:

    powershell.exe -version 2 .\test.ps1
    

    UPDATE

    If you want to inspect files before extracting, you'll need to recurse the directories yourself. Below is a demo of how this can be done.

    function New-ZipChildRootFolder 
    {
        param 
        (
            [string]$DestinationPath,
            [string]$ZipFileName
        )
    
        $folderPath = Split-Path -Path $ZipFileName -Leaf
    
        $destination = (New-Object -ComObject Shell.Application).NameSpace($DestinationPath)
    
        $destination.NewFolder($folderPath)
    }
    
    function Get-ZipChildItems 
    {
        param 
        (
            [string]$ZipFilePath,
            [string]$DestinationPath
        )
    
        $zipfile = (New-Object -ComObject Shell.Application).NameSpace($ZipFilePath)
        $zipFileName = [System.IO.Path]::GetFileNameWithoutExtension($ZipFilePath)
    
        Write-Output "Create root zip folder : $zipFileName"
        New-ZipChildRootFolder -DestinationPath $DestinationPath -ZipFileName $zipFileName
    
        foreach ($item in $zipFile.items()) 
        {
            Get-ZipChildItemsRecurse -Items $item -DestinationPath $DestinationPath -ZipFileName $zipFileName
        }
    }
    
    function Get-ZipChildItemsRecurse
    {
        param 
        (
            [object]$Items,
            [string]$DestinationPath,
            [string]$ZipFileName
        )
    
        foreach ($file in $Items.getFolder.Items())
        {
            if ($file.IsFolder -eq $true) 
            {
                Write-Output "Creating folder : $($file.Path)"
                New-ZipChildFolder -Folder $file -DestinationPath $DestinationPath -ZipFileName $ZipFileName
                Get-ZipChildItemsRecurse -Items $file -DestinationPath $DestinationPath -ZipFileName $ZipFileName
            }
            else 
            {
                $filename = Split-Path -Path $file.Path -Leaf
                if ($filename -eq "log.txt")
                {
                    Write-Output "Copying file : $($file.Path)"
                    New-ZipChildFile -File $file -DestinationPath $DestinationPath -ZipFileName $ZipFileName
                }
            }
        }
    }   
    
    function New-ZipChildFile
    {
        param
        (
            [object]$File,
            [string]$DestinationPath,
            [string]$ZipFileName
        )
    
        $destination = New-Object -ComObject Shell.Application
    
        $items = $File.Path.Split("\")
    
        $zipRootIndex = [array]::IndexOf($items, $ZipFileName)
    
        $path = $items[$zipRootIndex..($items.Length - 2)] -join "\"
    
        $fullPath = Join-path -Path $DestinationPath -ChildPath $path
    
        $destination.NameSpace($fullPath).CopyHere($File)
    }
    
    function New-ZipChildFolder
    {
        param 
        (
            [object]$Folder,
            [string]$DestinationPath,
            [string]$ZipFileName
        )
    
        $destination = New-Object -ComObject Shell.Application
    
        $items = $Folder.Path.Split("\")
    
        $zipRootIndex = [array]::IndexOf($items, $ZipFileName)
    
        $folders = $items[$zipRootIndex..($items.Length - 1)]
    
        $currentFolder = $DestinationPath
        foreach ($folder in $folders)
        {
            $destination.NameSpace($currentFolder).NewFolder($folder)
            $currentFolder = Join-Path -Path $currentFolder -ChildPath $folder
        }
    }
    

    Usage:

    $zipFilePath = "C:\Zipfile.zip"
    $destinationPath = "C:\Users\Public\Downloads"
    
    Get-ZipChildItems -ZipFile $zipFilePath -DestinationPath $destinationPath