Includes/Download.provider.url.ps1

<#
    Provider : url (direct download)
    Dependencies : PwSh.Fw.Web (Download-File)
 
    The local asset name is derived from the leaf of the URL path. When the
    URL does not point to a file (e.g. query-string urls like
    https://host/?product=...&os=win64), the name is taken from the
    Content-Disposition header of the response (HEAD request), falling back
    to '<codename>-<version>'.
    v1 limitation : no signature verification (no companion file that can
    be located by pattern).
#>


<#
.SYNOPSIS
Compute the local file name for a direct URL.
 
.DESCRIPTION
Fallback chain : leaf of the URL path → Content-Disposition filename from a
HEAD request → '<codename>-<version>'.
#>

function Get-AssetLocalName {
    [CmdletBinding()]
    [OutputType([string])]
    Param (
        [Parameter(Mandatory = $true)][string]$Url,
        # Fallback values when no name can be derived
        [Parameter(Mandatory = $false)][string]$Codename,
        [Parameter(Mandatory = $false)][string]$Version
    )
    Begin {
        Write-EnterFunction
    }

    Process {
        $uri = [uri]$Url
        $leaf = [uri]::UnEscapeDataString($uri.AbsolutePath.TrimEnd('/')) | Split-Path -Leaf
        if ($leaf) { return $leaf }

        try {
            $resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -MaximumRedirection 5 -ErrorAction Stop
            $cd = $resp.Headers['Content-Disposition']
            if ($cd -match 'filename\*?=[''"]?(?<name>[^''";]+)[''"]?') {
                $name = [uri]::UnEscapeDataString($Matches.name.Trim())
                if ($name) { return $name }
            }
        } catch {
            Write-Verbose "Could not derive asset name from Content-Disposition : $_"
        }

        if ($Codename -and $Version) { return "$Codename-$Version" }
        return 'download'
    }

    End {
        Write-LeaveFunction
    }
}

<#
.SYNOPSIS
Get asset information from a direct URL without downloading.
#>

function Get-AssetInfoFromUrl {
    [CmdletBinding()]
    [OutputType([array])]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef,
        [Parameter(Mandatory = $false)][string]$CachePath,
        [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{}
    )
    Begin {
        Write-EnterFunction
        $assets = @()
    }

    Process {
        $url = Render-MustacheField -value $pkgdef.download.url -mustacheValues $mustacheValues
        $fileName = Get-AssetLocalName -Url $url -Codename $pkgdef.codename -Version ($mustacheValues.VERSION ?? $pkgdef.version)

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

    End {
        Write-LeaveFunction
        return $assets
    }
}

<#
.SYNOPSIS
Download an asset from a direct URL.
 
.DESCRIPTION
Renders the mustache URL, computes the local file name, downloads the file
(skipping when already cached). Returns the local path.
#>

function Download-AssetFromUrl {
    [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 = @{}
    )
    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()
            }
        }

        $url = Render-MustacheField -value $pkgdef.download.url -mustacheValues $mustacheValues
        $fileName = Get-AssetLocalName -Url $url -Codename $pkgdef.codename -Version ($mustacheValues.VERSION ?? $pkgdef.version)
        $destFile = "$DestinationPath/$fileName"

        if (Test-FileExist $destFile) {
            Write-Verbose "Asset $fileName already exists in $DestinationPath, skipping download"
            return $destFile
        }

        Write-Verbose "Downloading $url to $destFile"
        $null = Download-File -Url $url -To $DestinationPath -Filename $fileName -PassThru
        if (-not (Test-FileExist $destFile)) {
            Write-Fatal "Error downloading asset from $url"
            return $null
        }
        return $destFile
    }

    End {
        Write-LeaveFunction
    }
}