Types/OpenPackage.Publisher/com.atproto.repo.uploadBlob.ps1

<#
.SYNOPSIS
    Publish Blobs to At Protocol
.DESCRIPTION
    Publishes content blobs to At Protocol.
.LINK
    https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/repo/uploadBlob.json
#>

[CmdletBinding(PositionalBinding=$false,SupportsShouldProcess)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    "PSAvoidUsingPlainTextForPassword", 
    "", 
    Justification="
    SecureStrings are not actually more secure.
    Use -Credential to avoid potential information disclosure in Windows event logs.
    "

)]
param(
# Handle or other identifier supported by the server for the authenticating user.
[string]
$Identifier,

# The app password or account password.
[string]
$AppPassword,

# A credential used to connect.
# The username will be treated as the `-Identifier`.
# The password will be treated as the `-AppPassword`
[Management.Automation.PSCredential]
[Alias('PSCredential')]
$Credential,

# The personal data server used for the connection.
[Alias('PersonalDataServer')]
[string]
$PDS = "https://bsky.social/",

# A content type map.
# This maps extensions and URIs to a content type.
[Collections.IDictionary]
$TypeMap = $(
    ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap
),

# Any input to publish.
# This can be:
# * A package part
# * A `[IO.FileInfo]` object.
# * A `[Collections.IDictionary]` containing .Content and .ContentType
[Parameter(ValueFromPipeline)]
[Alias('Package')]
[PSObject[]]
$InputObject
)

# Collect all piped input
$allInput = @($input)

# If that did not work, collect all non-piped `-InputObject`
if (-not $allInput) {
    $allInput += $InputObject
}

# If there is no input
if (-not $allInput) {
    # error out.
    Write-Error "No input to publish"
    return
}

# Declare a small function to upload our blobs
function uploadBlob {
    param(
    # The content to upload.
    # Can be bytes, files, or package parts.
    [Parameter(Mandatory)]
    [PSObject]
    $Content,
    
    # The content type.
    [string]
    $ContentType
    )    

    # Get our content as bytes
    [byte[]]$contentBytes =
        if ($content -is [byte[]] -or 
            $content -as [byte[]]
        ) {
            # If it is already bytes, or castable to bytes
            $content # use the content directly
        }
        # If if is a file
        elseif ($content -is [IO.FileInfo]) {
            # read all of the file bytes
            [IO.File]::ReadAllBytes($content.FullName)
            # and attempt to detect the correct content type
            if (-not $ContentType) {
                $contentType = if ($TypeMap.($content.Extension)) {
                    $TypeMap.($content.Extension)
                } else {
                    # defaulting to image/jpeg if missing
                    'image/jpeg'
                }
            }
        }
        # If the content is a stream and we can read it
        elseif (
            $content -is [IO.Stream] -and $content.CanRead
        ) {
            # seek to the start of the stream
            $null = $content.Seek('0', 'Begin')
            # copy to a memory stream
            $memoryStream = [IO.MemoryStream]::new()            
            $content.CopyTo($memoryStream)
            # get the bytes
            $memoryStream.ToArray()
            # and close up.
            $memoryStream.Close()
            $memoryStream.Dispose()
        }
        # If the content is a package part
        elseif ($content -is [IO.Packaging.PackagePart]) 
        {
            # use the content type of the package part
            if (-not $ContentType) {
                $contentType = $content.ContentType
            }
            # Copy the content to a memory stream
            $memoryStream = [IO.MemoryStream]::new()
            $contentStream = $content.GetStream('Open', 'Read')
            $contentStream.CopyTo($memoryStream)
            # get the bytes
            $memoryStream.ToArray()
            # and close up.
            $memoryStream.Close()
            $memoryStream.Dispose()
            $contentStream.Close()
            $contentStream.Dispose()
        }

    # If we could not get content as bytes
    if (-not $contentBytes) {
        # error out
        Write-Error "Could not get content as bytes"
        return
    }
    
    # Declare the namespace identifier and http method
    $NamespaceID = 'com.atproto.repo.uploadBlob'    
    $httpMethod  = 'POST'
    # And construct an upload url using our pds.
    $uploadUrl = "$(
        # If the PDS was already https
        if ($pds -like 'https://*') {
            # just trim trailing slashes from https urls.
            $pds -replace '/$'
        } else {
            # Otherwise, prefix anything else by https://
            "https://$pds" -replace '/$'
    })/xrpc/$NamespaceID"
    

    # Prepare our invoke parameters
    $invokeSplat = [Ordered]@{
        Uri = $uploadUrl
        Body = $contentBytes
        Method = $httpMethod
        ContentType = $ContentType
    }

    # If -WhatIf was passed,
    if ($WhatIfPreference) {
        return $invokeSplat # output our invoke parameters
    }
    # Otherwise, add the authentication header
    $invokeSplat.Headers = [Ordered]@{Authorization="Bearer $($atConnection.accessJwt)"}
    # and call the endpoint.
    Invoke-RestMethod @invokeSplat
}

# Reset any potential connection.
$atConnection = $null
# and then see if we have enough data to connect.
if (-not $atConnection -and (
    ($Identifier -and $appPassword) -or 
    ($Credential)
)) {
    # If we do, prepare a splat
    $connectionSplat = [Ordered]@{}
    if ($identifier -and $AppPassword) {
        $connectionSplat.Identifier = $Identifier
        $connectionSplat.AppPassword = $AppPassword
    }
    else {
        $connectionSplat.Credential = $Credential
    }

    # and connect.
    $atConnection = Publish-OpenPackage -Publisher com.atproto.server.createSession -Option $connectionSplat
}

#region Upload Blobs
$InputNumber = 0
:nextInput foreach ($in in $allInput) {
    if ($in -is [Collections.IDictionary]) {
        if (-not (
            ($in.Content -is [byte[]]) -or 
            ($in.Content -as [byte[]]) -or
            ($in.Content -is [IO.Stream] -and $in.Content.CanRead) -or
            ($in.Content -is [IO.FileInfo]) -or
            ($in.Content -is [IO.Packaging.PackagePart])
        )) {
            Write-Warning "Input # $InputNumber must contain Content"
            continue
        }
        if ((-not $in.ContentType) -and (
            $in.Content -isnot [IO.FileInfo] -and
            $in.Content -isnot [IO.Packaging.PackagePart]
        )) {
            Write-Warning "Input # $InputNumber must contain ContentType"
            continue
        }
        $inCopy = [Ordered]@{}
        $inCopy.Content = $in.Content
        $inCopy.ContentType = $in.ContentType
        uploadBlob @inCopy
        continue
    }
    elseif ($in -is [IO.FileInfo] -or 
        $in -is [IO.Packaging.PackagePart] ) {
        uploadBlob -Content $in
        continue
    }
    
    $InputNumber++
}
#endregion Upload Blobs