Wsl-Image/Wsl-Image.Cmdlets.ps1

function New-WslImageHash {
    <#
    .SYNOPSIS
    Creates a new FileSystem hash holder.

    .DESCRIPTION
    The WslImageHash object holds checksum information for one or more
    images in order to check it upon download and determine if the filesystem
    has been updated.

    Note that the checksums are not downloaded until the `Retrieve()` method has been
    called on the object.

    .PARAMETER Url
    The Url where the checksums are located.

    .PARAMETER Algorithm
    The checksum algorithm. Nowadays, we find mostly SHA256.

    .PARAMETER Type
    Type can either be `sums` in which case the file contains one
    <checksum> <filename> pair per line, or `single` and just contains the hash for
    the file which name is the last segment of the Url minus the extension. For
    instance, if the URL is `https://.../rootfs.tar.xz.sha256`, we assume that the
    checksum it contains is for the file named `rootfs.tar.xz`.

    .EXAMPLE
    New-WslImageHash https://cloud-images.ubuntu.com/wsl/noble/current/SHA256SUMS
    Creates the hash source for several files with SHA256 (default) algorithm.

    .EXAMPLE
    New-WslImageHash https://.../rootfs.tar.xz.sha256 -Type `single`
    Creates the hash source for the rootfs.tar.xz file with SHA256 (default) algorithm.

    .NOTES
    General notes
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([WslImageHash])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Url,
        [Parameter(Mandatory = $false)]
        [string]$Algorithm = 'SHA256',
        [Parameter(Mandatory = $false)]
        [string]$Type = 'sums'
    )

    return [WslImageHash]@{
        Url       = $Url
        Algorithm = $Algorithm
        Type      = $Type
    }

}

function New-WslImage {
    <#
    .SYNOPSIS
    Creates a WslImage object.

    .DESCRIPTION
    WslImage object retrieve and provide information about available root
    filesystems.

    .PARAMETER Name
    The identifier of the image. It can be an already known name:
    - Arch
    - Alpine
    - Ubuntu
    - Debian

    It also can be the URL (https://...) of an existing filesystem or a
    image name saved through Export-WslInstance.

    It can also be a URL in the form:

        incus://<os>#<release> (ex: incus://rockylinux#9)

    In this case, it will fetch the last version the specified image in
    https://images.linuxcontainers.org/images.

    .PARAMETER Path
    The path of the root filesystem. Should be a file ending with `rootfs.tar.gz`.
    It will try to extract the OS and Release from the filename (in /etc/os-release).

    .PARAMETER File
    A FileInfo object of the compressed root filesystem.

    .EXAMPLE
    New-WslImage incus:alpine:3.19
        Type Os Release State Name
        ---- -- ------- ----- ----
        Incus alpine 3.19 Synced incus.alpine_3.19.rootfs.tar.gz
    The WSL root filesystem representing the incus alpine 3.19 image.

    .EXAMPLE
    New-WslImage alpine -Configured
        Type Os Release State Name
        ---- -- ------- ----- ----
    Builtin Alpine 3.19 Synced miniwsl.alpine.rootfs.tar.gz
    The builtin configured Alpine root filesystem.

    .EXAMPLE
    New-WslImage test.rootfs.tar.gz
        Type Os Release State Name
        ---- -- ------- ----- ----
    Builtin Alpine 3.21.3 Synced test.rootfs.tar.gz
    The The root filesystem from the file.

    .LINK
    Get-WslImage
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([WslImage])]
    param (
        [Parameter(Position = 0, ParameterSetName = 'Name', Mandatory = $true)]
        [string]$Name,
        [Parameter(ParameterSetName = 'Path', ValueFromPipeline = $true, Mandatory = $true)]
        [string]$Path,
        [Parameter(ParameterSetName = 'File', ValueFromPipeline = $true, Mandatory = $true)]
        [FileInfo]$File
    )

    process {
        if ($PSCmdlet.ParameterSetName -eq "Name") {
            return [WslImage]::new($Name)
        }
        else {
            if ($PSCmdlet.ParameterSetName -eq "Path") {
                $Path = Resolve-Path $Path
                $File = [FileInfo]::new($Path)
            }
            return [WslImage]::new($File)
        }
    }

}

function Sync-WslImage {
    <#
    .SYNOPSIS
    Synchronize locally the specified WSL root filesystem.

    .DESCRIPTION
    If the root filesystem is not already present locally, downloads it from its
    original URL.

    .PARAMETER Name
    The identifier of the image. It can be an already known name:
    - Arch
    - Alpine
    - Ubuntu
    - Debian

    It also can be the URL (https://...) of an existing filesystem or a
    image name saved through Export-WslInstance.

    It can also be a name in the form:

        incus://<os>#<release> (ex: incus://rockylinux#9)

    In this case, it will fetch the last version the specified image in
    https://images.linuxcontainers.org/images.

    It can also designate a docker image in the form:

        docker://<registry>/<image>#<tag> (ex: docker://ghcr.io/antoinemartin/yawsldocker/yawsldocker-alpine:latest)

    NOTE: Currently, only images with a single layer are supported.

    .PARAMETER Image
    The WslImage object to process.

    .PARAMETER Force
    Force the synchronization even if the root filesystem is already present locally.

    .INPUTS
    The WslImage Objects to process.

    .OUTPUTS
    The WslImage objects.

    .EXAMPLE
    Sync-WslImage Alpine -Configured
    Syncs the already configured builtin Alpine root filesystem.

    .EXAMPLE
    Sync-WslImage Alpine -Force
    Re-download the Alpine builtin root filesystem.

    .EXAMPLE
    Get-WslImage -State NotDownloaded -Os Alpine | Sync-WslImage
    Synchronize the Alpine root filesystems not already synced

    .EXAMPLE
     New-WslImage alpine -Configured | Sync-WslImage | % { &wsl --import test $env:LOCALAPPDATA\Wsl\test $_ }
     Create a WSL distro from a synchronized root filesystem.

    .LINK
    New-WslImage
    Get-WslImage
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([WslImage])]
    param (
        [Parameter(Position = 0, ParameterSetName = 'Name', Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Image")]
        [WslImage[]]$Image,
        [Parameter(Mandatory = $true, ParameterSetName = "Path")]
        [string]$Path,
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    process {

        if ($PSCmdlet.ParameterSetName -eq "Name") {
            $Image = $Name | ForEach-Object { New-WslImage -Name $_ }
        }
        if ($PSCmdlet.ParameterSetName -eq "Path") {
            $Image = New-WslImage -Path $Path
        }

        if ($null -ne $Image) {
            $Image | ForEach-Object {
                $fs = $_
                [FileInfo] $dest = $fs.File

                If (!([WslImage]::BasePath.Exists)) {
                    if ($PSCmdlet.ShouldProcess([WslImage]::BasePath.Create(), "Create base path")) {
                        [WslImage]::BasePath.Create()
                    }
                }

                if (!$dest.Exists -Or $_.Outdated -Or $true -eq $Force) {
                    if ($PSCmdlet.ShouldProcess($fs.Url, "Sync locally")) {
                        try {
                            $fs.FileHash = $fs.GetHashSource().DownloadAndCheckFile($fs.Url, $fs.File)
                        }
                        catch [Exception] {
                            throw [WslManagerException]::new("Error while loading distro [$($fs.OsName)] on $($fs.Url): $($_.Exception.Message)", $_.Exception)
                        }
                        $fs.State = [WslImageState]::Synced
                        $fs.WriteMetadata()
                        Success "[$($fs.OsName)] Synced at [$($dest.FullName)]."
                    }
                }
                else {
                    Information "[$($fs.OsName)] Root FS already at [$($dest.FullName)]."
                }

                return $fs
            }

        }
    }

}


function Get-WslImage {
    <#
    .SYNOPSIS
        Gets the WSL root filesystems installed on the computer and the ones available.
    .DESCRIPTION
        The Get-WslImage cmdlet gets objects that represent the WSL root filesystems available on the computer.
        This can be the ones already synchronized as well as the Builtin filesystems available.
    .PARAMETER Name
        Specifies the name of the filesystem.
    .PARAMETER Os
        Specifies the Os of the filesystem.
    .PARAMETER Type
        Specifies the type of the filesystem.
    .PARAMETER Outdated
        Return the list of outdated images. Works mainly on Builtin images.
    .INPUTS
        System.String
        You can pipe a image name to this cmdlet.
    .OUTPUTS
        WslImage
        The cmdlet returns objects that represent the WSL root filesystems on the computer.
    .EXAMPLE
        Get-WslImage
           Type Os Release State Name
           ---- -- ------- ----- ----
        Builtin Alpine 3.19 NotDownloaded alpine.rootfs.tar.gz
        Builtin Arch current Synced arch.rootfs.tar.gz
        Builtin Debian bookworm Synced debian.rootfs.tar.gz
          Local Docker unknown Synced docker.rootfs.tar.gz
          Local Flatcar unknown Synced flatcar.rootfs.tar.gz
        Incus almalinux 8 Synced incus.almalinux_8.rootfs.tar.gz
        Incus almalinux 9 Synced incus.almalinux_9.rootfs.tar.gz
        Incus alpine 3.19 Synced incus.alpine_3.19.rootfs.tar.gz
        Incus alpine edge Synced incus.alpine_edge.rootfs.tar.gz
        Incus centos 9-Stream Synced incus.centos_9-Stream.Image.ta...
        Incus opensuse 15.4 Synced incus.opensuse_15.4.rootfs.tar.gz
        Incus rockylinux 9 Synced incus.rockylinux_9.rootfs.tar.gz
        Builtin Alpine 3.19 Synced miniwsl.alpine.rootfs.tar.gz
        Builtin Arch current Synced miniwsl.arch.rootfs.tar.gz
        Builtin Debian bookworm Synced miniwsl.debian.rootfs.tar.gz
        Builtin Opensuse tumbleweed Synced miniwsl.opensuse.rootfs.tar.gz
        Builtin Ubuntu noble NotDownloaded miniwsl.ubuntu.rootfs.tar.gz
          Local Netsdk unknown Synced netsdk.rootfs.tar.gz
        Builtin Opensuse tumbleweed Synced opensuse.rootfs.tar.gz
          Local Out unknown Synced out.rootfs.tar.gz
          Local Postgres unknown Synced postgres.rootfs.tar.gz
        Builtin Ubuntu noble Synced ubuntu.rootfs.tar.gz
        Get all WSL root filesystem.

    .EXAMPLE
        Get-WslImage -Os alpine
           Type Os Release State Name
           ---- -- ------- ----- ----
        Builtin Alpine 3.19 NotDownloaded alpine.rootfs.tar.gz
          Incus alpine 3.19 Synced incus.alpine_3.19.rootfs.tar.gz
          Incus alpine edge Synced incus.alpine_edge.rootfs.tar.gz
        Builtin Alpine 3.19 Synced miniwsl.alpine.rootfs.tar.gz
        Get All Alpine root filesystems.
    .EXAMPLE
        Get-WslImage -Type Incus
        Type Os Release State Name
        ---- -- ------- ----- ----
        Incus almalinux 8 Synced incus.almalinux_8.rootfs.tar.gz
        Incus almalinux 9 Synced incus.almalinux_9.rootfs.tar.gz
        Incus alpine 3.19 Synced incus.alpine_3.19.rootfs.tar.gz
        Incus alpine edge Synced incus.alpine_edge.rootfs.tar.gz
        Incus centos 9-Stream Synced incus.centos_9-Stream.Image.ta...
        Incus opensuse 15.4 Synced incus.opensuse_15.4.rootfs.tar.gz
        Incus rockylinux 9 Synced incus.rockylinux_9.rootfs.tar.gz
        Get All downloaded Incus root filesystems.
    #>

    [CmdletBinding()]
    [OutputType([WslImage])]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$Name,
        [Parameter(Mandatory = $false)]
        [string]$Os,
        [Parameter(Mandatory = $false)]
        [WslImageSource]$Source = [WslImageSource]::Local,
        [Parameter(Mandatory = $false)]
        [WslImageState]$State,
        [Parameter(Mandatory = $false)]
        [WslImageType]$Type,
        [Parameter(Mandatory = $false)]
        [switch]$Configured,
        [Parameter(Mandatory = $false)]
        [switch]$Outdated
    )

    process {
        $fileSystems = @()
        if ($Source -band [WslImageSource]::Local) {
            $fileSystems += [WslImage]::LocalFileSystems()
        }
        if ($Source -band [WslImageSource]::Builtins) {
            $fileSystems += Get-WslBuiltinImage -Source Builtins
        }
        if ($Source -band [WslImageSource]::Incus) {
            $fileSystems += Get-WslBuiltinImage -Source Incus
        }
        $fileSystems = $fileSystems | Sort-Object | Select-Object -Unique

        if ($PSBoundParameters.ContainsKey("Type")) {
            $fileSystems = $fileSystems | Where-Object {
                $_.Type -eq $Type
            }
        }

        if ($PSBoundParameters.ContainsKey("Os")) {
            $fileSystems = $fileSystems | Where-Object {
                $_.Os -eq $Os
            }
        }

        if ($PSBoundParameters.ContainsKey("State")) {
            $fileSystems = $fileSystems | Where-Object {
                $_.State -eq $State
            }
        }

        if ($PSBoundParameters.ContainsKey("Configured")) {
            $fileSystems = $fileSystems | Where-Object {
                $_.Configured -eq $Configured.IsPresent
            }
        }

        if ($PSBoundParameters.ContainsKey("Outdated")) {
            $fileSystems = $fileSystems | Where-Object {
                $_.Outdated
            }
        }

        if ($Name.Length -gt 0) {
            $fileSystems = $fileSystems | Where-Object {
                foreach ($pattern in $Name) {
                    Write-Verbose "Checking pattern: $pattern against $($_.Name)"
                    if ($_.Name -ilike $pattern -or $_.Name -imatch "(\w+\.)?$pattern\.rootfs\.tar\.gz") {
                        return $true
                    }
                }

                return $false
            }
            if ($null -eq $fileSystems) {
                throw [UnknownWslImageException]::new($Name)
            }
        }

        return $fileSystems
    }
}


<#
.SYNOPSIS
Remove a WSL root filesystem from the local disk.

.DESCRIPTION
If the WSL root filesystem in synced, it will remove the tar file and its meta
data from the disk. Builtin root filesystems will still appear as output of
`Get-WslImage`, but their state will be `NotDownloaded`.

.PARAMETER Distribution
The identifier of the image. It can be an already known name:
- Arch
- Alpine
- Ubuntu
- Debian

It also can be the URL (https://...) of an existing filesystem or a
image name saved through Export-WslInstance.

It can also be a name in the form:

    incus:<os>:<release> (ex: incus:rockylinux:9)

In this case, it will fetch the last version the specified image in
https://images.linuxcontainers.org/images.


.PARAMETER Image
The WslImage object representing the WSL root filesystem to delete.

.INPUTS
One or more WslImage objects representing the WSL root filesystem to
delete.

.OUTPUTS
The WslImage objects updated.

.EXAMPLE
Remove-WslImage alpine -Configured
Removes the builtin configured alpine root filesystem.

.EXAMPLE
New-WslImage "incus:alpine:3.19" | Remove-WslImage
Removes the Incus alpine 3.19 root filesystem.

.EXAMPLE
Get-WslImage -Type Incus | Remove-WslImage
Removes all the Incus root filesystems present locally.

.Link
Get-WslImage
New-WslImage
#>

Function Remove-WslImage {
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([WslImage])]
    param (
        [Parameter(Position = 0, ParameterSetName = 'Name', Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Image")]
        [WslImage[]]$Image
    )

    process {

        if ($PSCmdlet.ParameterSetName -eq "Name") {
            $Image = Get-WslImage -Name $Name
        }

        if ($null -ne $Image) {
            $Image | ForEach-Object {
                if ($_.Delete()) {
                    $_
                }
            }
        }
    }
}