PSDependScripts/FileDownload.ps1

# cspell:ignore extensionless sqllite
<#
    .SYNOPSIS
        Download a file
 
    .DESCRIPTION
        Download a file
 
        Relevant Dependency metadata:
            DependencyName (Key): The key for this dependency is used as the URL. This can be overridden by 'Source'
            Name: Optional file name for the downloaded file. Defaults to parsing filename from the URL
            Target: The folder to download this file to. If a full path to a new file is used, this overrides any other file name.
            Source: Optional override for URL
            AddToPath: If specified, prepend the target's parent container to PATH
 
    .PARAMETER PSDependAction
        Test or Install the module. Defaults to Install
 
        Test: Return true or false on whether the dependency is in place
        Install: Install the dependency
 
    .EXAMPLE
        sqllite_dll = @{
            DependencyType = 'FileDownload'
            Source = 'https://github.com/RamblingCookieMonster/PSSQLite/blob/main/PSSQLite/x64/System.Data.SQLite.dll?raw=true'
            Target = 'C:\temp'
        }
 
        # Downloads System.Data.SQLite.dll to C:\temp
 
    .EXAMPLE
        'https://github.com/RamblingCookieMonster/PSSQLite/blob/main/PSSQLite/x64/System.Data.SQLite.dll?raw=true' = @{
            DependencyType = 'FileDownload'
            Target = 'C:\temp\sqlite.dll'
        }
 
        # Downloads System.Data.SQLite.dll to C:\temp\sqlite.dll
#>

[CmdletBinding()]
param(
    [PSTypeName('PSDepend.Dependency')]
    [PSObject[]]
    $Dependency,

    [ValidateSet('Test', 'Install')]
    [string[]]$PSDependAction = @('Install')
)

function Parse-URLForFile {
    [CmdletBinding()]
    param($URL)
    # This will need work. Assume leaf is file. If CGI exists in leaf, assume it is after the file
    $FileName = $URL.split('/')[-1]
    if ($FileName -match '\?') {
        $FileName.split('?')[0]
    }
    else {
        $FileName
    }
    Write-Verbose "Parsed file name [$FileName] from `$URL"
}

# Extract data from Dependency
$DependencyName = $Dependency.DependencyName
$Name = $Dependency.Name
$Target = $Dependency.Target
$Source = $Dependency.Source

# Pick the URL
if ($Source) {
    $URL = $Source
}
else {
    $URL = $DependencyName
}
Write-Verbose "Using URL: $URL"

# Act on target path....
$ToInstall = $False # Anti pattern

# Capture trailing-separator intent before GetUnresolvedProviderPathFromPSPath normalizes it away;
# a trailing separator is an explicit signal that the target is a container, not a file.
$endsWithSeparator = $Target -and
    $Target[-1] -in @([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)

# Resolve PSDrive paths (e.g. TestDrive:) and relative paths to absolute filesystem paths
$Target = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Target)

$TargetParent = Split-Path $Target -Parent
$PathToAdd = $Target

if (Test-Path $Target -PathType Leaf) {
    # File exists. We should download to temp spot, compare hashes, take action as appropriate.
    # For now, skip the file.
    Write-Verbose "Skipping existing file [$Target]"
    if ($PSDependAction -contains 'Test') {
        return $True
    }
    $PathToAdd = Split-Path $Target -Parent
}
elseif (
    -not $endsWithSeparator -and
    -not (Test-Path $Target -PathType Container) -and
    ([IO.Path]::GetExtension($Target) -or (Test-Path $TargetParent))
) {
    # Treat as a full destination file path.
    # Triggered when: has a file extension, OR parent already exists and caller gave no trailing separator.
    # The trailing-separator check preserves extensionless binary targets (e.g. /usr/local/bin/terraform)
    # while still allowing directory-like targets to be signalled with a trailing slash.
    $PathToAdd = $TargetParent
    if (-not (Test-Path $TargetParent)) {
        Write-Error "Could not find parent path [$TargetParent] for target [$Target]"
        if ($PSDependAction -contains 'Test') {
            return $False
        }
        return
    }
    $Path = $Target
    $ToInstall = $True
    Write-Verbose "Treating as destination file path [$Target]"
}
else {
    # No extension (or already a container, or explicit trailing separator) — treat target as a directory
    Write-Verbose "[$Target] is a container, creating path to file"
    if (-not (Test-Path $Target) -and $PSDependAction -contains 'Install') {
        New-Item -ItemType Directory -Path $Target -Force | Out-Null
    }
    If ($Name) {
        # explicit name
        $FileName = $Name
    }
    else {
        $FileName = Parse-URLForFile -URL $URL
    }
    $Path = Join-Path $Target $FileName

    if (Test-Path $Path -PathType Leaf) {
        Write-Verbose "Skipping existing file [$Path]"
        if ($PSDependAction -contains 'Test') {
            return $True
        }
    }
    else {
        $ToInstall = $True
    }
}
Write-Verbose "Using [$Path] as `$Target"

#No dependency found, return false if we're testing alone...
if ( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) {
    return $False
}
Write-Verbose "Downloading [$URL] to [$Path]"

if ($PSDependAction -contains 'Install' -and $ToInstall) {
    # Future considerations:
    # Should we check for existing? And if we find it, still download file, and compare sha256 hash, replace if it does not match?
    # We should consider credentials at some point, but PSD1 does not lend itself to securely storing passwords
    Get-WebFile -URL $URL -Path $Path
}

if ($Dependency.AddToPath) {
    Write-Verbose "Setting PATH to`n$($PathToAdd, $env:PATH -join [IO.Path]::PathSeparator | Out-String)"
    Add-ToItemCollection -Reference Env:\Path -Item $PathToAdd
}