files.ps1

# Nabil Redmann - 2026-04-02
# License: MIT

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '', Scope = 'Function', Target = '*')] # allow aliases
[CmdletBinding()] # allow -Debug
Param() # no parameters, but required
$script:DoDebug = $DebugPreference # for internal use - activate in all functions ("inherited" from parent (this) script) - to use: `. .\files.ps1 -Debug`
Write-Debug "Loading PollinationsAiPS/files.ps1"


$script:BaseUri = "https://media.pollinations.ai"


Function Add-PollinationsAiFile {
    [CmdletBinding()]
    param(
        [string]
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        $Path,
        
        [string]
        [Alias("key")]
        $POLLINATIONSAI_API_KEY = $env:POLLINATIONSAI_API_KEY,
        
        [switch]
        $Details
    )

    begin {
        $global:LASTEXITCODE = 0
        if ($script:DoDebug) { $DebugPreference = $script:DoDebug }
        if (-not $POLLINATIONSAI_API_KEY) { throw "⚠️ POLLINATIONSAI API KEY is missing! (-key or -POLLINATIONSAI_API_KEY or set `$env:POLLINATIONSAI_API_KEY=`"sk_...`")" ; return $null }
    }

    process {
        if (-not (Test-Path $Path -PathType Leaf)) {
            throw "Cannot find file path '$Path'. Only local file paths are supported for raw binary upload."
            return $null # in case of -ErrorAction SilentlyContinue
        }

        if ((Get-Item $Path).Length -le 10MB) {
            throw "File size max is 10MB"
            return $null
        }

        $localPath = Resolve-Path $Path

        Write-Debug "Uploading file: $localPath"

        $uri = "$($script:BaseUri)/upload"
        $headers = @{ Authorization = "Bearer $POLLINATIONSAI_API_KEY" }

        $response,$err,$json = script:IWR -Uri $uri -Method Post -Headers $headers -InFile $localPath -filterTextHeaders $false # we expect JSON

        $url = $null
        $contentJson = @{}
        if (-not $err) {
            Write-Debug "File uploaded successfully"

            $contentJson = $json
            $url = if ($contentJson.url) { $contentJson.url.ToString().Trim() } else { $response.Content.Trim() }
        }

        Write-Debug "File $($contentJson.id) URL: $url"

        if ($Details) { 
            $ret = @{
                id = $contentJson.id
                hash = $contentJson.id
                uri = $(if ($url) { $url } else { $uri })
                contentType = $contentJson.contentType
                size = $contentJson.size
                duplicate = $contentJson.duplicate
                Headers = $response.Headers
                Content = $response.Content
                StatusCode = $response.StatusCode
            }
            if ($err) { $ret += @{ error = $err} }
        }
        else {
            if ($err) {
                $ret = $null # in case of -ErrorAction SilentlyContinue
            }
            else {
                $ret = $url
            }
        }

        return $ret
    }
}


# Throws on 404! This is on purpose
Function Get-PollinationsAiFile {
    [CmdletBinding(DefaultParameterSetName="None")]
    param(
        [string]
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0, ParameterSetName='None')]
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0, ParameterSetName='WithOut')]
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0, ParameterSetName='WithSave')]
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0, ParameterSetName='WithDetails')]
        $Hash,
        
        [string]
        [Parameter(Mandatory=$false, ParameterSetName='None')]
        [Parameter(Mandatory=$false, ParameterSetName='WithOut')]
        [Parameter(Mandatory=$false, ParameterSetName='WithSave')]
        [Parameter(Mandatory=$false, ParameterSetName='WithDetails')]
        [Alias("key")]
        $POLLINATIONSAI_API_KEY = $env:POLLINATIONSAI_API_KEY,

        [switch]
        [Parameter(Mandatory=$true, ParameterSetName='WithDetails')]
        [Parameter(Mandatory=$false, ParameterSetName='WithOut')]
        [Parameter(Mandatory=$false, ParameterSetName='WithSave')]
        $Details = $false,

        [switch]
        [Parameter(Mandatory=$true, ParameterSetName='WithSave')]
        $Save = $false,

        [string]
        [Parameter(Mandatory=$true, ParameterSetName='WithOut')]
        $Out = ""
    )

    begin {
        $global:LASTEXITCODE = 0
        if ($script:DoDebug) { $DebugPreference = $script:DoDebug }
        #? OPTIONAL: if (-not $POLLINATIONSAI_API_KEY) { throw "⚠️ POLLINATIONSAI API KEY is missing! (-key or -POLLINATIONSAI_API_KEY or set `$env:POLLINATIONSAI_API_KEY=`"sk_...`")" ; return $null }
    }

    process {
        $headers = @{ "Content-Type" = "application/json" }
        if ($POLLINATIONSAI_API_KEY) { $headers += @{Authorization = "Bearer $POLLINATIONSAI_API_KEY"} } # optional

        $uri = "$($script:BaseUri)/$Hash"

        $ret = ""
        $response,$err,$json = script:IWR -Uri $uri -Method Get -Headers $headers
        

        $filepath = $null
        # error: no file to save
        if (($out -ne "" -or $save -eq $true) -and $err) {  # in case of -ErrorAction SilentlyContinue
            $filepath = "" # property should be available, but empty
            Write-Debug "Filepath: no file saved due to an error"
        }
        # save the File
        elseif ($out -ne "" -or $save -eq $true) {

            if ($out -eq "") {
                #! Content-Disposition and X-Request-ID is always empty
                #! we need to use the $hash
                $targetFilename = $Hash
                
                # dir is temp dir
                $targetDir = [IO.Path]::GetTempPath()
                
                $filepath = [IO.Path]::Combine($targetDir, $targetFilename)
            }
            else {
                $filepath = [IO.Path]::Combine($PWD, $out)
            }
            
            # check .jpg and ignore case
            if ($response.Headers["Content-Type"] -eq "image/jpeg" -and -not $filepath.EndsWith(".jpg", [System.StringComparison]::OrdinalIgnoreCase)) {
                $filepath += ".jpg"
            }

            # unknown file type - results in this form encoded -- it should be application/octet-stream
            if (($response.Headers["Content-Type"] -eq "application/x-www-form-urlencoded" -or $response.Headers["Content-Type"] -eq "application/octet-stream") -and $out -eq "") {
                
                $ext = script:Get-FileTypeFromBytes $response.Content
                if ($ext) {
                    $filepath += "." + $ext
                }
                else {
                    Write-Warning "PollinationsAI API returned for file '$Hash' a wrong Content-Type of '$($response.Headers["Content-Type"])' and no filename was provided, the file type can not be determined - no extension added. Use -Out parameter to specify the filename."
                }
            }
            # png and others, like video
            elseif ($Null -eq (Split-Path $filepath -Leaf).Split(".")[1]) { #PWSH 6+ "" -eq (Split-Path $filepath -Leaf | Split-Path -Extension)
                $type = $response.Headers["Content-Type"] -split ";" | Select -First 1 |% { $_ -split "/"} | Select -Last 1
                $filepath += "." + $type
            }
            
            Write-Debug "Filepath: $filepath"
            
            # save the File
            [IO.File]::WriteAllBytes($filepath, $response.Content)
        }

        # return details
        if ($Details -eq $true) {
            $ret = @{
                id = $response.Headers.'X-Content-Hash' -or $Hash
                hash = $Hash
                uri = $uri
                contentType = $response.Headers.'Content-Type'
                size = $response.Headers.'Content-Length'
                Headers = $response.Headers
                Content = $response.Content
                StatusCode = $response.StatusCode
            }
            if ($null -ne $filepath) { $ret += @{ filePath = $filepath } } # filename is available (or an empty string)
            if ($err) { $ret += @{ error = $err} }
        }
        # return filepath
        elseif ($Save) { $ret = $filepath }
        # return file's content
        else {
            if ($err) { 
                $ret = $null # in case of -ErrorAction SilentlyContinue
            }
            else {
                $ret = $response.Content
            }
        }
        return $ret
    }
}


Function Remove-PollinationsAiFile {
    [CmdletBinding()]
    param(
        [string]
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        $Hash,

        [string]
        [Alias("key")]
        $POLLINATIONSAI_API_KEY = $env:POLLINATIONSAI_API_KEY,
        
        [switch]
        $Details
    )

    begin {
        $global:LASTEXITCODE = 0
        if ($script:DoDebug) { $DebugPreference = $script:DoDebug }
        if (-not $POLLINATIONSAI_API_KEY) { throw "⚠️ POLLINATIONSAI API KEY is missing! (-key or -POLLINATIONSAI_API_KEY or set `$env:POLLINATIONSAI_API_KEY=`"sk_...`")" ; return $null }
    }

    process {
        $headers = @{ "Content-Type" = "application/json"; Authorization = "Bearer $POLLINATIONSAI_API_KEY" }
        $uri = "$($script:BaseUri)/$Hash"

        $response,$err,$json = script:IWR -Uri $uri -Method Delete -Headers $headers -ErrorAction SilentlyContinue -filterTextHeaders $false # we expect JSON, we WANT the error info

        $contentJson = @{}
        if (-not $err) {
            $contentJson = $json
            Write-Debug "File deleted: $($contentJson.deleted)"
        }

        if ($Details) { 
            $ret = @{
                deleted = $contentJson.deleted
                id = $contentJson.id
                hash = if ($contentJson.id) { $contentJson.id } else { $Hash }
                uri = $uri
                Headers = $response.Headers
                Content = $response.Content
                StatusCode = $response.StatusCode
            }
            if ($err) { $ret += @{ error = $err} }
        }
        else {
            if ($err) {
                $ret = $false
            }
            else {
                $ret = $true
            }
        }

        return $ret
    }
}


#! ENDPOINT BUGGY - https://github.com/pollinations/pollinations/pull/10449
Function Export-PollinationsAiFile {
    [CmdletBinding()]
    param(
        [string]
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        $Hash,
        
        [string]
        [Alias("key")]
        $POLLINATIONSAI_API_KEY = $env:POLLINATIONSAI_API_KEY,
        
        [switch]
        $Details
    )

    begin {
        $global:LASTEXITCODE = 0
        if ($script:DoDebug) { $DebugPreference = $script:DoDebug }
        #? OPTIONAL: if (-not $POLLINATIONSAI_API_KEY) { throw "⚠️ POLLINATIONSAI API KEY is missing! (-key or -POLLINATIONSAI_API_KEY or set `$env:POLLINATIONSAI_API_KEY=`"sk_...`")" ; return $null }
    }

    process {
        $headers = @{ "Content-Type" = "application/json" }
        if ($POLLINATIONSAI_API_KEY) { $headers += @{Authorization = "Bearer $POLLINATIONSAI_API_KEY"} }

        $uri = "$($script:BaseUri)/$Hash/metadata"

        $response,$err,$json = script:IWR -Uri $uri -Method Get -Headers $headers -ErrorAction SilentlyContinue -filterTextHeaders $false # we expect JSON, we WANT the error info

        if ($Details) {
            $ret = @{
                id = $Hash
                hash = $Hash
                uri = $uri
                Headers = $response.Headers
                Content = $response.Content
                data = $json
                StatusCode = $response.StatusCode
            }
            if ($err) { $ret += @{ error = $err} }
        }
        else {
            if ($err -and $err.StatusCode -eq 404) {   # file not found info
                $ret = $false
            }
            elseif ($err) {         # not a file info -> auth or something else
                $ret = $null
            }
            else {
                $ret = $json
            }
        }

        return $ret
    }

}


Function Test-PollinationsAiFile {
    [CmdletBinding()]
    param(
        [string]
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        $Hash,

        [string]
        [Alias("key")]
        $POLLINATIONSAI_API_KEY = $env:POLLINATIONSAI_API_KEY,

        [switch]
        $Details
    )

    begin {
        $global:LASTEXITCODE = 0
        if ($script:DoDebug) { $DebugPreference = $script:DoDebug }
        #? OPTIONAL: if (-not $POLLINATIONSAI_API_KEY) { throw "⚠️ POLLINATIONSAI API KEY is missing! (-key or -POLLINATIONSAI_API_KEY or set `$env:POLLINATIONSAI_API_KEY=`"sk_...`")" ; return $null }
    }

    process {
        $headers = @{ "Content-Type" = "application/json" }
        if ($POLLINATIONSAI_API_KEY) { $headers += @{Authorization = "Bearer $POLLINATIONSAI_API_KEY"} }

        $uri = "$($script:BaseUri)/$Hash"

        $response,$err,$json = script:IWR -Uri $uri -Method Head -Headers $headers -ErrorAction SilentlyContinue # we WANT the error info

        Write-Debug "File $Hash exists: $(-not $err)"

        if ($Details) { 
            $ret = @{
                success = (-not $err)
                id = $Hash # API is missing headers: X-Content-Hash,X-Content-Size
                hash = $Hash
                uri = $uri
                contentType = if (-not $err) { $response.Headers.'Content-Type' }
                contentLength = if (-not $err) { $response.Headers.'Content-Length' }
                Headers = $response.Headers
                StatusCode = $response.StatusCode
            }
            if ($err) { $ret += @{ error = $err} }
        }
        else {
            if ($err -and $err.StatusCode -eq 404) {   # file not found info
                $ret = $false
            }
            elseif ($err) {         # not a file info -> auth or something else
                $ret = $null
            }
            else {
                $ret = $true   # file found info
            }
        }

        return $ret
    }
}


Function Get-PollinationsAiEncodedImage {
    # `data:image/png;base64,` or `data:image/jpeg;base64,`
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)][string]$Path,
        [switch]$Details
    )

    begin {
        $global:LASTEXITCODE = 0
        if ($script:DoDebug) { $DebugPreference = $script:DoDebug }
    }

    process {
        try {
            Add-Type -AssemblyName System.Web
            
            $filePath = Resolve-Path $Path
            $bytes = [System.IO.File]::ReadAllBytes($filePath)
            $encodedImage = [System.Convert]::ToBase64String($bytes)
            
            # check $bytes the file content, what content type the image has (GetMimeMapping won't work)
            $type = ""
            if ($bytes[0] -eq 0x89 -and $bytes[1] -eq 0x50 -and $bytes[2] -eq 0x4E -and $bytes[3] -eq 0x47) { $type = "png" }
            if ($bytes[0] -eq 0xFF -and $bytes[1] -eq 0xD8 -and $bytes[2] -eq 0xFF -and $bytes[3] -eq 0xE0) { $type = "jpeg" }
            if ($bytes[0] -eq 0xFF -and $bytes[1] -eq 0xD8 -and $bytes[2] -eq 0xFF -and $bytes[3] -eq 0xE1) { $type = "jpeg" }
            if ($bytes[0] -eq 0xFF -and $bytes[1] -eq 0xD8 -and $bytes[2] -eq 0xFF -and $bytes[3] -eq 0xE2) { $type = "jpeg" }
            if ($bytes[0] -eq 0xFF -and $bytes[1] -eq 0xD8 -and $bytes[2] -eq 0xFF -and $bytes[3] -eq 0xE3) { $type = "jpeg" }
            if ($bytes[0] -eq 0x42 -and $bytes[1] -eq 0x4D) { $type = "bmp" }

            if ($type -eq "") { throw "File is not an jpeg/png/bmp image!" }

            Write-Debug "Image type: $type"

            # content
            $content = "data:image/$type;base64,$encodedImage"
            if ($Details) { 
                return @{
                    id = $Hash
                    hash = $Hash
                    Content = $content
                    contentType = "image/$type"
                    path = $filePath
                }
            }
            else { return $content }
        }
        catch { throw $_ }
    }
}


<#
    .Notes
        To run: PS> . Measure-PollinationsAiFile -Details
 
        Allows to dig into the vars, eg. $result
#>

Function Measure-PollinationsAiFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false)][string]$PathTestImage,
        [string][Alias("key")]$POLLINATIONSAI_API_KEY = $env:POLLINATIONSAI_API_KEY,
        [switch]$KeepLocal,
        [switch]$Details
    )

    process {
        if (-not $POLLINATIONSAI_API_KEY) { throw "⚠️ POLLINATIONSAI API KEY is missing! (-key or -POLLINATIONSAI_API_KEY or set `$env:POLLINATIONSAI_API_KEY=`"sk_...`")" }

        $createdTemp = $false
        try {
            if ([string]::IsNullOrWhiteSpace($PathTestImage)) {
                # use stable online test image instead of an ad hoc text file
                $PathTestImage = Join-Path ([IO.Path]::GetTempPath()) ("pollinationsaitest_{0}.png" -f [guid]::NewGuid())
                $sourceUrl = "https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg/960px-Wikipedia-logo-v2.svg.png"
                Invoke-WebRequest -Uri $sourceUrl -OutFile $PathTestImage -UseBasicParsing -ErrorAction Stop
                $createdTemp = $true
            } else {
                if (-not (Test-Path $PathTestImage -PathType Leaf)) {
                    throw "Path not found: $PathTestImage"
                }
            }

            # Test Upload
            $upload = Add-PollinationsAiFile -Path $PathTestImage -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY -Details
            if ($null -eq $upload) {
                # fallback in case Function returns URL only
                $url = Add-PollinationsAiFile -Path $PathTestImage -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY
                $hash = $url -replace '^.*/',''
            } else {
                # upload is a hashtable with id/hash/url/etc
                $hash = $upload.hash
                if (-not $hash) {
                    $hash = $upload.url -replace '^.*/',''
                }
            }

            # Test with -Details
            $check1Detail = $null
            $get1Detail = $null
            $meta1Detail = $null
            $rm1Detail = $null
            $check2Detail = $null
            
            try { $check1Detail = Test-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY -Details } 
            catch { Write-Warning "Test-PollinationsAiFile (before, -Details) failed: $_" }
            
            try { $get1Detail = Get-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY -Details } 
            catch { Write-Warning "Get-PollinationsAiFile (-Details) failed: $_" }
            
            try { $meta1Detail = Export-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY -Details } 
            catch { Write-Warning "Export-PollinationsAiFile (-Details) failed: $_" }

            # Test without -Details (basic return value test)
            $check1NoDetail = $null
            $get1NoDetail = $null
            $meta1NoDetail = $null
            
            try { $check1NoDetail = Test-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY } 
            catch { Write-Warning "Test-PollinationsAiFile (before, no -Details) failed: $_" }
            
            try { $get1NoDetail = Get-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY } 
            catch { Write-Warning "Get-PollinationsAiFile (no -Details) failed: $_" }
            
            try { $meta1NoDetail = Export-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY } 
            catch { Write-Warning "Export-PollinationsAiFile (no -Details) failed: $_" }

            # Remove file
            $rm1Detail = $null
            $rm1NoDetail = $null
            try { $rm1Detail = Remove-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY -Details } 
            catch { Write-Warning "Remove-PollinationsAiFile (-Details) failed: $_" }
            
            try { $rm1NoDetail = Remove-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY } 
            catch { Write-Warning "Remove-PollinationsAiFile (no -Details) failed: $_" }

            # Test after deletion
            $check2NoDetail = $null
            $check2Detail = $null
            try { $check2Detail = Test-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY -Details } 
            catch { Write-Warning "Test-PollinationsAiFile (after, -Details) failed: $_" }
            
            try { $check2NoDetail = Test-PollinationsAiFile -Hash $hash -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY } 
            catch { Write-Warning "Test-PollinationsAiFile (after, no -Details) failed: $_" }

            # Determine success: file existed before, doesn't exist after
            $beforeExists = if ($check1Detail) { $null -ne $check1Detail.ContentType } else { $check1NoDetail -eq $true }
            $afterExists = if ($check2Detail) { $null -ne $check2Detail.ContentType } else { $check2NoDetail -eq $true }
            $successFlag = $beforeExists -and (-not $afterExists)

            $result = [pscustomobject]@{
                InputPath      = $PathTestImage
                Uploaded       = $true
                Hash           = $hash
                UploadResult   = $upload
                TestBefore    = $check1Detail
                TestBeforeNoDetail = $check1NoDetail
                GetResult      = if ($Details) { $get1Detail } else { $null }
                GetResultNoDetail = if ($Details) { $get1NoDetail } else { $null }
                Metadata       = if ($Details) { $meta1Detail } else { $null }
                MetadataNoDetail = if ($Details) { $meta1NoDetail } else { $null }
                RemoveResult   = $rm1Detail
                RemoveResultNoDetail = $rm1NoDetail
                TestAfter      = $check2Detail
                TestAfterNoDetail = $check2NoDetail
                Success        = $successFlag
                MultiFileUpload = $null
                MultiFileRemove = $null
                MultiFileUploadSuccess = $false
                MultiFileRemoveSuccess = $false
            }

            # additional test: multi-file upload via pipeline + multi-hash remove via pipeline
            $multiFileUrls = @(
                "https://upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/500px-Commons-logo.svg.png",
                "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Enwiki-25.svg/500px-Enwiki-25.svg.png"
            )
            $tmpFiles = @()
            try {
                foreach ($u in $multiFileUrls) {
                    $tmp = Join-Path ([IO.Path]::GetTempPath()) ("pollinationsaitest_{0}.png" -f [guid]::NewGuid())
                    Invoke-WebRequest -Uri $u -OutFile $tmp -UseBasicParsing -ErrorAction Stop
                    $tmpFiles += $tmp
                }

                $multiUpload = $tmpFiles | Add-PollinationsAiFile -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY -Details
                $multiHashes = $multiUpload | ForEach-Object { $_.hash }
                $multiRemove = $multiHashes | Remove-PollinationsAiFile -POLLINATIONSAI_API_KEY $POLLINATIONSAI_API_KEY -Details

                $result.MultiFileUpload = $multiUpload
                $result.MultiFileRemove = $multiRemove
                $result.MultiFileUploadSuccess = ($multiUpload.Count -eq $tmpFiles.Count)
                $result.MultiFileRemoveSuccess = ($multiRemove | Where-Object { $_ -and $_.deleted } | Measure-Object).Count -eq $tmpFiles.Count
            } catch {
                Write-Warning "Multi-file pipeline test failed: $_"
                $result.MultiFileUploadSuccess = $false
                $result.MultiFileRemoveSuccess = $false
            } finally {
                foreach ($t in $tmpFiles) { Remove-Item -Path $t -ErrorAction SilentlyContinue }
            }

            if ($Details) { return $result } else { Write-Host "Success: $([string]$result.Success)"; return $true }
        } finally {
            if ($createdTemp -and -not $KeepLocal -and (Test-Path $PathTestImage)) {
                Remove-Item $PathTestImage -Force -ErrorAction SilentlyContinue
            }
        }
    }
}


<#
    Invoke-WebRequest fallback for PowerShell 5.1 (but looses the error message from within the response's content)
#>

Function script:IWR {
    param(
        [Parameter(Mandatory=$true)]$Uri,
        $Method = "Get",
        $Headers = @{},
        $filterTextHeaders = $true,   # in case we expect JSON, set filterTextHeaders to false
        
        $InFile = $null
    )

    if ($script:DoDebug) { $DebugPreference = $script:DoDebug }

    Write-Debug "Request $Method $uri"

    
    # prepare additional (known) parameters, if needed
    if ($InFile) {
        $params = @{ InFile = $InFile }
    }
    else {
        $params = @{}
    }
    
    # check for PowerShell 7+
    $canSkip = (Get-Command Invoke-WebRequest).Parameters.ContainsKey('SkipHttpErrorCheck')

    if ($canSkip) {
        $response = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers   @params   -UseBasicParsing     -SkipHttpErrorCheck    # get the error message in the response
    }
    else {
        # Fallback for PowerShell 5.1 --> does only show the status code, since the response is dropped by Invoke-WebRequest
        try {
            $response = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers   @params   -UseBasicParsing     -ErrorAction Stop
        }
        catch {
            # no message from content possible on error in PS 5.1 ... Headers are just the keynames and no values
            #$response = @{ StatusCode = $_.Exception.Response.StatusCode.Value__; Message = $_.Exception.Response.StatusCode; Headers = $_.Exception.Response.Headers }
            
            # get the content properly and extract headers ---------
            $stream = $_.Exception.Response.GetResponseStream()
            if ($stream.CanSeek) { $stream.Position = 0 }

            # Read into a MemoryStream to get all bytes
            $memoryStream = New-Object System.IO.MemoryStream
            $stream.CopyTo($memoryStream)
            $bytes = $memoryStream.ToArray()
            $memoryStream.Dispose()
            $contentString = [System.Text.Encoding]::UTF8.GetString($bytes)
            
            # Extract Headers properly, WebHeaderCollection -> Hashtable
            $headers = @{}
            foreach ($key in $_.Exception.Response.Headers.AllKeys) { $headers[$key] = $_.Exception.Response.Headers[$key] }

            $response = [PSCustomObject]@{
                StatusCode = [int]$_.Exception.Response.StatusCode
                Content    =  $contentString
                #RawBytes = $bytes # The binary version
                Headers    = $headers
            }
        }
    }

    # check for errors (image endpoint format and media storage endpoint format)
    $err = $null
    if ($response.StatusCode -ne 200) {
        Write-Debug "Error status code: $($response.StatusCode)"
        if ($response.Content -and $response.Headers["Content-Type"] -like "application/json") {
            $content = $response.Content | ConvertFrom-Json
            $err = [PSCustomObject]@{
                StatusCode = if ($content.status) {$content.status} else {$response.StatusCode}
                Message = if ($content.error -and $content.error.message) {$content.error.message} else {$content.error}
                ResponseInstance = $response
            }
        }
        else {
            $err = [PSCustomObject]@{
                StatusCode = $response.StatusCode
                Message = $response.Content
                Uri = $Uri
                ResponseInstance = $response
            }
        }

        Write-Debug "Error message: $($err.Message)"
        #Write-Error "$($err.StatusCode): $($err.Message)"

        # set error code
        $global:LASTEXITCODE = $err.StatusCode
        throw $err
    }
    # API is broken - Status code 200, but there is an error message in the content
    elseif ($filterTextHeaders -and ($response.Headers["Content-Type"] -like "text/*" -or $response.Headers["Content-Type"] -like "application/json")) {
        Write-Debug "Error unexpected content with content type: $($response.Headers["Content-Type"])"
        $err = [PSCustomObject]@{
            StatusCode = $response.StatusCode
            Message = "Unexpected content with content type: " + $response.Headers["Content-Type"]
            Uri = $Uri
            ResponseInstance = $response
        }

        Write-Debug "Error content: $($response.Content)"
        # Write-Error $response.Content
        # Write-Error ($err.StatusCode + ": " + $err.Message)

        # set error code
        $global:LASTEXITCODE = 200
        throw $err
    }

    # check for JSON, if $filterTextHeaders is false or -ErrorAction is SilentlyContinue
    $json = $null
    if ($response.Headers["Content-Type"] -like "application/json") {
        $json = $response.Content | ConvertFrom-Json
    }

    # application/x-www-form-urlencoded .... storage api returns this
    # ... the content is bytes (currently)

    return $response, $err, $json
}


<#
    Get file extension from bytes - Advanced Windows version or simple fallback
#>

Function script:Get-FileTypeFromBytes {
    param (
        [byte[]]$Content
    )

    # I bumped this to 12 to prevent strict-mode index errors,
    # since WAV and AVI check up to the 11th byte.
    if ($null -eq $Content -or $Content.Count -lt 12) {
        return $false
    }

    # Extract the first few bytes for comparison
    # We use a switch with -CaseSensitive or simple byte matching
    switch -Regex ($null) {
        # JPEG: FF D8 FF
        { $Content[0] -eq 0xFF -and $Content[1] -eq 0xD8 -and $Content[2] -eq 0xFF } { "jpg" }
        
        # PNG: 89 50 4E 47
        { $Content[0] -eq 0x89 -and $Content[1] -eq 0x50 -and $Content[2] -eq 0x4E -and $Content[3] -eq 0x47 } { "png" }
        
        # BMP: 42 4D
        { $Content[0] -eq 0x42 -and $Content[1] -eq 0x4D } { "bmp" }
        
        # AVI: 52 49 46 46 (RIFF) followed by 'AVI ' at offset 8
        { $Content[0] -eq 0x52 -and $Content[1] -eq 0x49 -and $Content[2] -eq 0x46 -and $Content[3] -eq 0x46 -and 
          $Content[8] -eq 0x41 -and $Content[9] -eq 0x56 -and $Content[10] -eq 0x49 } { "avi" }

        # WAV: 52 49 46 46 (RIFF) followed by 'WAVE' at offset 8
        { $Content[0] -eq 0x52 -and $Content[1] -eq 0x49 -and $Content[2] -eq 0x46 -and $Content[3] -eq 0x46 -and 
          $Content[8] -eq 0x57 -and $Content[9] -eq 0x41 -and $Content[10] -eq 0x56 -and $Content[11] -eq 0x45 } { "wav" }
        
        # MPG (MPEG): 00 00 01 BA or 00 00 01 B3
        { $Content[0] -eq 0x00 -and $Content[1] -eq 0x00 -and $Content[2] -eq 0x01 -and ($Content[3] -eq 0xBA -or $Content[3] -eq 0xB3) } { "mpg" }
        
        # HEIF: Looks for 'ftyp' at offset 4
        { $Content[4] -eq 0x66 -and $Content[5] -eq 0x74 -and $Content[6] -eq 0x79 -and $Content[7] -eq 0x70 } { "heif" }

        # MP3 (With ID3 Tag): 49 44 33 ("ID3")
        { $Content[0] -eq 0x49 -and $Content[1] -eq 0x44 -and $Content[2] -eq 0x33 } { "mp3" }

        # MP3 (Without ID3 Tag, MPEG ADTS sync word): FF FB, FF F3, or FF F2
        { $Content[0] -eq 0xFF -and ($Content[1] -eq 0xFB -or $Content[1] -eq 0xF3 -or $Content[1] -eq 0xF2) } { "mp3" }

        # AC3: 0B 77
        { $Content[0] -eq 0x0B -and $Content[1] -eq 0x77 } { "ac3" }
        
        Default { $null }
    }
}