functions/Copy-FilesFromContainerImage.ps1

# <copyright file="Copy-FilesFromContainerImage.ps1" company="Endjin Limited">
# Copyright (c) Endjin Limited. All rights reserved.
# </copyright>
function Copy-FilesFromContainerImage {
    <#
    .SYNOPSIS
        Copies files from a specified container image to a destination path on the local system.
 
    .DESCRIPTION
        Copies one or more files from a given container image and to a specified destination path on the host system.
         
        It has two modes of operation:
        - 'Files' mode: copies specific files from the container image to the destination path, using the same file
                        copy semantics as the `Copy-Item` cmdlet.
        - 'Archive' mode: copies the entire container filesystem as a tarball to the destination path.
 
    .PARAMETER SourceImageName
        The full repository, name and tag name of the container image from which the root filesystem will be used to
        create the disk image.
 
    .PARAMETER DestinationFolderPath
        The base directory where the specified files will be saved.
 
    .EXAMPLE
        Copy-FilesFromContainerImage -SourceImageName "mycontainer:latest" `
                                     -DestinationFolderPath "C:\Images" `
                                     -AllFilesAsTar
                                      
        This command copies a tarball containing all files from the `mycontainer:latest` container to
        `C:\Images\mycontainer-latest.tar`
 
    .EXAMPLE
        Copy-FilesFromContainerImage -SourceImageName "mycontainer:latest" `
                                     -DestinationFolderPath "C:\Images" `
                                     -Filter *.bin `
                                     -Recurse
                                      
        This command recursively copies all files matching the `*.bin` filter from the `mycontainer:latest`
        docker image to `C:\Images`.
    .NOTES
        This function requires Docker to be installed and running on the host system.
    #>


    [CmdletBinding(DefaultParameterSetName = 'Files')]
    param (
        [Parameter(Mandatory=$true)]
        [string] $SourceImageName,
        
        [Parameter(Mandatory=$true)]
        [string] $DestinationFolderPath,

        [Parameter(Mandatory=$true, ParameterSetName="Archive")]
        [switch] $AllFilesAsTar,

        [Parameter(ParameterSetName="Files")]
        [string] $Filter,

        [Parameter(ParameterSetName="Files")]
        [string[]] $Include,

        [Parameter(ParameterSetName="Files")]
        [string[]] $Exclude,

        [Parameter()]
        [switch] $OverwriteDestination,

        [Parameter(ParameterSetName="Files")]
        [switch] $Recurse,

        [Parameter()]
        [switch] $AlwaysPull,

        [Parameter()]
        [switch] $PassThru,

        [Parameter()]
        [string] $ContainerCommand = "/bin/sh"
    )

    @(
        "docker"
        "tar"
    ) | ForEach-Object {
        if (!(Get-Command $_ -ErrorAction Ignore)) {
            throw "The '$_' command is required to run this function"
        }
    }

    $existingImage = & docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object { $_ -eq $SourceImageName }
    if (!$existingImage -or $AlwaysPull) {
        Write-Host "Pulling image: $SourceImageName"
        & docker pull $SourceImageName | Write-Verbose
    }

    $tmpContainerName = (New-Guid).Guid
    $containerTarPath = Join-Path ([IO.Path]::GetTempPath()) "$tmpContainerName.tar"
    $tmpContainerExtractPath = Join-Path ([IO.Path]::GetTempPath()) "$tmpContainerName-extract"
    New-Item -ItemType Directory -Path $tmpContainerExtractPath -Force:$OverwriteDestination | Out-Null

    # Ensure errors from external tools are propagated to the script - this overridden
    # value will fall out of scope when the function completes
    $PSNativeCommandUseErrorActionPreference = $true

    # Create an instance of the container image
    & docker create --name $tmpContainerName $SourceImageName $ContainerCommand | Out-Null
    try {
        # Extract filesystem from container instance as a tarball
        Write-Host "Exporting container filesystem to tarball..."
        & docker export $tmpContainerName -o $containerTarPath | Write-Verbose

        $splat = @{
            Destination = $DestinationFolderPath
            Force = $OverwriteDestination
            PassThru = $PassThru
            Verbose = $VerbosePreference
        }
        switch ($PSCmdlet.ParameterSetName) {
            'Files' {
                Write-Host "Copying specified files from container filesystem"

                # Unpack files from the tarball
                & tar -xf $containerTarPath -C $tmpContainerExtractPath | Write-Verbose

                # Copying required files
                $splat += @{
                    Path = (Join-Path $tmpContainerExtractPath ([IO.Path]::DirectorySeparatorChar) "*")
                    Recurse = $Recurse
                }
                if ($Filter) { $splat.Filter = $Filter }
                if ($Include) { $splat.Include = $Include }
                if ($Exclude) { $splat.Exclude = $Exclude }
            }

            'Archive' {
                Write-Host "Copying tarball of entire container filesystem"
                $splat += @{
                    Path = $containerTarPath
                }
            }
        }

        $copiedFiles = Copy-Item @splat
    }
    finally {
        # Clean-up temporary resources
        if (Test-Path $containerTarPath) {
            Remove-Item $containerTarPath -Force
        }
        if (Test-Path $containerTarPath) {
            Remove-Item $tmpContainerExtractPath -Force
        }
        & docker rm -f $tmpContainerName | Write-Verbose

    }

    if ($PassThru) { return $copiedFiles }
}