Includes/Download.provider.gitlab.ps1

<#
    Provider : gitlab (API v4)
    Dependencies : none (the GitLab API is called directly via Invoke-RestMethod)
 
    The token is read from the environment variable named by
    download.token_env (never stored in the definition). Self-hosted
    instances via download.gitlab_url (default https://gitlab.com).
#>


<#
.SYNOPSIS
Resolve the GitLab release matching the requested version.
 
.DESCRIPTION
'latest' uses the permalink/latest endpoint ; any other value matches the
first release whose tag contains it (substring match). TAGNAME is set in
the mustache values table. Returns the release object.
#>

function Get-ReleaseFromGitlab {
    [CmdletBinding()]
    [OutputType([object])]
    Param (
        # Resolved package definition
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        # Template values (uppercase keys, mustache compatible)
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{},
        # GitLab base URL (default: gitlab.com)
        [Parameter(Mandatory = $false)][string]$GitLabBaseUrl = "https://gitlab.com",
        # GitLab API token
        [Parameter(Mandatory = $false)][string]$GitLabToken
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        $projectEncoded = [Uri]::EscapeDataString("$($pkgdef.download.owner)/$($pkgdef.download.repo)")
        $headers = @{}
        if ($GitLabToken) { $headers["PRIVATE-TOKEN"] = $GitLabToken }

        $release = $null
        if ($pkgdef.version -eq "latest") {
            $url = "$GitLabBaseUrl/api/v4/projects/$projectEncoded/releases/permalink/latest"
            $release = Invoke-RestMethod -Uri $url -Headers $headers -Method Get
        } else {
            $url = "$GitLabBaseUrl/api/v4/projects/$projectEncoded/releases"
            $releases = Invoke-RestMethod -Uri $url -Headers $headers -Method Get
            $release = @($releases | Where-Object { $_.tag_name -like "*$($pkgdef.version)*" }) | Select-Object -First 1
        }

        if ($release) {
            Write-Verbose "Found release $($release.tag_name) matched requested version $($pkgdef.version)"
        } else {
            Write-Fatal "No release found for version $($pkgdef.version)"
        }

        if ($mustacheValues) { $mustacheValues.TAGNAME = $release.tag_name }
        return $release
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Select the asset of a GitLab release matching the ordered pattern list.
 
.DESCRIPTION
GitLab stores uploaded files under release.assets.links. The returned asset
is normalized to the shape expected downstream : @{ name ; browser_download_url }.
#>

function Get-AssetFromGitlab {
    [CmdletBinding()]
    [OutputType([hashtable])]
    Param (
        # Resolved package definition
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        # Template values (uppercase keys, mustache compatible)
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{},
        # Release found with Get-ReleaseFromGitlab
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][System.Object]$Release
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        $assets = @($Release.assets.links)
        if (-not $assets.Count) {
            Write-Fatal "No assets found for release $($Release.tag_name)"
        }
        Write-Verbose "Found $($assets.Count) assets for release $($Release.tag_name)"

        $match = Select-GenericPackageAsset -Names (@($assets | ForEach-Object { $_.name })) -Assets $assets -AssetSpecs @($pkgdef.download.assets) -mustacheValues $mustacheValues
        if (-not $match) {
            Write-Fatal "No asset of release $($Release.tag_name) matches the declared patterns"
        }

        $asset = $assets[$match.Index]
        return @{
            name                 = $asset.name
            browser_download_url = $asset.direct_asset_url ?? $asset.url
        }
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Get asset information from a GitLab release without downloading.
#>

function Get-AssetInfoFromGitlab {
    [CmdletBinding()]
    [OutputType([array])]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        [Parameter(Mandatory = $false)][string]$CachePath,
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{},
        [Parameter(Mandatory = $false)][string]$GitLabBaseUrl = "https://gitlab.com",
        [Parameter(Mandatory = $false)][string]$GitLabToken
    )
    Begin {
        Write-EnterFunction
        $assets = @()
    }

    Process {
        $release = Get-ReleaseFromGitlab -pkgdef $pkgdef -mustacheValues $mustacheValues -GitLabBaseUrl $GitLabBaseUrl -GitLabToken $GitLabToken
        $asset = Get-AssetFromGitlab -pkgdef $pkgdef -mustacheValues $mustacheValues -Release $release
        if ($asset) {
            $assets += @{
                name         = $asset.name
                download_url = $asset.browser_download_url
                local_path   = "$CachePath/$($asset.name)"
            }
        }
    }

    End {
        Write-LeaveFunction
        return $assets
    }
}

<#
.SYNOPSIS
Download an asset from a GitLab release.
 
.DESCRIPTION
Resolves the release, selects the asset with the ordered pattern list,
downloads it (skipping when already cached) with the auth header for private
instances, and verifies the signature-file when declared.
#>

function Download-AssetFromGitlab {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Download is a more intuitive verb for this function because Get- is too generic.")]
    [CmdletBinding()]
    [OutputType([String])]
    Param (
        # Resolved package definition
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        # Destination path (cache directory)
        [Parameter()][String]$DestinationPath,
        # Template values (uppercase keys, mustache compatible)
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{},
        # GitLab base URL (default: gitlab.com)
        [Parameter(Mandatory = $false)][string]$GitLabBaseUrl = "https://gitlab.com",
        # GitLab API token
        [Parameter(Mandatory = $false)][string]$GitLabToken
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        # Resolve DestinationPath: parameter > pkgdef.download.destination > temp path
        if ([string]::IsNullOrEmpty($DestinationPath)) {
            if ($pkgdef.download.destination) {
                $DestinationPath = $pkgdef.download.destination
            } else {
                $DestinationPath = [io.path]::GetTempPath()
            }
        }

        $release = Get-ReleaseFromGitlab -pkgdef $pkgdef -mustacheValues $mustacheValues -GitLabBaseUrl $GitLabBaseUrl -GitLabToken $GitLabToken
        $asset = Get-AssetFromGitlab -pkgdef $pkgdef -mustacheValues $mustacheValues -Release $release
        if (-not $asset) { return $null }

        $destFile = "$DestinationPath/$($asset.name)"
        if (Test-FileExist $destFile) {
            Write-Verbose "Asset $($asset.name) already exists in $DestinationPath, skipping download"
            return $destFile
        }

        # Inject auth header for private instances
        $downloadHeaders = @{}
        if ($GitLabToken) { $downloadHeaders["PRIVATE-TOKEN"] = $GitLabToken }

        Write-Verbose "Downloading $($asset.name) from $($asset.browser_download_url) to $DestinationPath"
        $null = Download-File -Url $asset.browser_download_url -To $DestinationPath -Headers $downloadHeaders -PassThru
        if (-not (Test-FileExist $destFile)) {
            Write-Fatal "Error downloading asset $($asset.name)"
            return $null
        }

        # Signature-file verification (companion link from the same release)
        $spec = $null
        foreach ($a in @($pkgdef.download.assets)) {
            if ($a.pattern -and $asset.name -match (Render-MustacheField -value $a.pattern -mustacheValues $mustacheValues)) {
                $spec = $a
                break
            }
        }
        if ($spec -and $spec.'signature-file') {
            $sigPattern = Render-MustacheField -value $spec.'signature-file'.pattern -mustacheValues $mustacheValues
            $sigLink = @($release.assets.links | Where-Object { $_.name -match $sigPattern }) | Select-Object -First 1
            if (-not $sigLink) {
                Write-Fatal "Signature file matching '$sigPattern' not found in release $($release.tag_name)"
                return $null
            }
            $sigUrl = $sigLink.direct_asset_url ?? $sigLink.url
            $sigFile = Download-File -Url $sigUrl -To $DestinationPath -Headers $downloadHeaders -PassThru
            if (-not (Test-GenericPackageSignature -AssetPath $destFile -SignaturePath $sigFile -Algorithm $spec.'signature-file'.algorithm)) {
                return $null
            }
        }

        return $destFile
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Get artifact information from GitLab CI without downloading.
#>

function Get-AssetInfoFromGitlabArtifact {
    [CmdletBinding()]
    [OutputType([array])]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        [Parameter(Mandatory = $false)][string]$CachePath,
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{},
        [Parameter(Mandatory = $false)][string]$GitLabBaseUrl = "https://gitlab.com"
    )
    Begin {
        Write-EnterFunction
        $assets = @()
    }

    Process {
        $projectEncoded = [Uri]::EscapeDataString("$($pkgdef.download.owner)/$($pkgdef.download.repo)")
        $ref  = $pkgdef.download.artifact.ref
        $job  = $pkgdef.download.artifact.job
        $artifactPath = $pkgdef.download.artifact.path

        if (-not $ref) { Write-Fatal "artifact.ref is required in package definition" }
        if (-not $job) { Write-Fatal "artifact.job is required in package definition" }

        if ($artifactPath) {
            $artifactPathEncoded = $artifactPath.TrimStart("/")
            $download_url = "$GitLabBaseUrl/api/v4/projects/$projectEncoded/jobs/artifacts/$ref/raw/$artifactPathEncoded`?job=$job"
            $fileName = Split-Path $artifactPath -Leaf
        } else {
            $download_url = "$GitLabBaseUrl/api/v4/projects/$projectEncoded/jobs/artifacts/$ref/download?job=$job"
            $fileName = "$($pkgdef.download.repo)-$($pkgdef.name)-artifacts.zip"
        }

        $download_url = Render-MustacheField -value $download_url -mustacheValues $mustacheValues
        $fileName     = Render-MustacheField -value $fileName -mustacheValues $mustacheValues

        $assets += @{
            name         = $fileName
            download_url = $download_url
            local_path   = "$CachePath/$fileName"
        }
    }

    End {
        Write-LeaveFunction
        return $assets
    }
}

<#
.SYNOPSIS
Download a CI artifact from GitLab.
 
.DESCRIPTION
Downloads an artifact produced by a GitLab CI pipeline job :
  GET /projects/:id/jobs/artifacts/:ref/download?job=:job_name
or, for a specific file inside the artifact :
  GET /projects/:id/jobs/artifacts/:ref/raw/*artifact_path?job=:job_name
#>

function Download-ArtifactFromGitlab {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Download is a more intuitive verb for this function because Get- is too generic.")]
    [CmdletBinding()]
    [OutputType([String])]
    Param (
        # Resolved package definition
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        # Destination path (cache directory)
        [Parameter()][String]$DestinationPath,
        # Template values (uppercase keys, mustache compatible)
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{},
        # GitLab base URL (default: gitlab.com)
        [Parameter(Mandatory = $false)][string]$GitLabBaseUrl = "https://gitlab.com",
        # GitLab API token
        [Parameter(Mandatory = $false)][string]$GitLabToken
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        # Resolve DestinationPath: parameter > pkgdef.download.destination > temp path
        if ([string]::IsNullOrEmpty($DestinationPath)) {
            if ($pkgdef.download.destination) {
                $DestinationPath = $pkgdef.download.destination
            } else {
                $DestinationPath = [io.path]::GetTempPath()
            }
        }

        $headers = @{}
        if ($GitLabToken) { $headers["PRIVATE-TOKEN"] = $GitLabToken }

        $projectEncoded = [Uri]::EscapeDataString("$($pkgdef.download.owner)/$($pkgdef.download.repo)")
        $ref  = $pkgdef.download.artifact.ref
        $job  = $pkgdef.download.artifact.job
        $artifactPath = $pkgdef.download.artifact.path

        if (-not $ref) { Write-Fatal "artifact.ref is required in package definition" }
        if (-not $job) { Write-Fatal "artifact.job is required in package definition" }

        if ($artifactPath) {
            # Download one specific file inside the artifact
            $artifactPathEncoded = $artifactPath.TrimStart("/")
            $download_url = "$GitLabBaseUrl/api/v4/projects/$projectEncoded/jobs/artifacts/$ref/raw/$artifactPathEncoded`?job=$job"
            $fileName = Split-Path $artifactPath -Leaf
        } else {
            # Download the full artifact zip of the job
            $download_url = "$GitLabBaseUrl/api/v4/projects/$projectEncoded/jobs/artifacts/$ref/download?job=$job"
            $fileName = "$($pkgdef.download.repo)-$($pkgdef.name)-artifacts.zip"
        }

        # Mustache resolution on the url and the name when needed
        $download_url = Render-MustacheField -value $download_url -mustacheValues $mustacheValues
        $fileName     = Render-MustacheField -value $fileName -mustacheValues $mustacheValues

        $destFile = "$DestinationPath/$fileName"

        if (Test-FileExist $destFile) {
            Write-Verbose "Artifact $fileName already exists in $DestinationPath, skipping download"
        } else {
            Write-Verbose "Downloading artifact $fileName from $download_url to $DestinationPath"
            $null = Download-File -Url $download_url -To $DestinationPath -Headers $headers -PassThru
        }

        Write-Devel "downloaded artifact = $destFile"

        if (Test-FileExist $destFile) {
            return $destFile
        } else {
            Write-Fatal "Error downloading artifact $fileName"
            return $null
        }
    }

    End {
        Write-LeaveFunction
    }
}