Commands/Set-Neocities.ps1

function Set-Neocities
{
    <#
    .SYNOPSIS
        Sets Neocities files
    .DESCRIPTION
        Sets files on Neocities website using the neocities API.
    #>

    [CmdletBinding(DefaultParameterSetName='upload')]
    param(
    # The path to the file to upload, or a dictionary of files and their contents.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('Fullname','FilePath','Path')]
    [PSObject]
    $File,    
    
    # The neocities credential
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias(
        'Credentials', # Plural aliases are nice
        'PSCredential', # so are parameters that match the type name.
        'NeocitiesCredential', # A contextual alias is a good idea, too.
        'NeocitiesCredentials' # And you may need to pluralize that contextual alias.
    )]
    [PSCredential]
    $Credential,

    # The access token
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $AccessToken
    )

    begin {
        $NeocitiesApi = "https://neocities.org/api"
        $multiparts = [Ordered]@{}
        $boundary = "boundary"
        $contentType = "multipart/form-data; boundary=`"$boundary`""
        
    }
    process {
        $parameterSet = $PSCmdlet.ParameterSetName
        $psuedoNamespace = "neocities"
        $pseudoType = "$parameterSet"
        $InvokeSplat = [Ordered]@{
            Uri = "$NeocitiesApi", $PSCmdlet.ParameterSetName -join '/'
        }
        
        # If an access token was provided
        if ($AccessToken)
        {
            # use it
            $InvokeSplat.Headers = @{Authorization = "Bearer $AccessToken"}
            # and cache it for later use
            $script:NeocitiesAccessToken = $AccessToken
        } 
        elseif ($Credential)
        {
            # If a credential was provided, use it
            $InvokeSplat.Credential = $Credential
            # and cache it for later use
            $script:NeoCitiesCredential = $Credential
            # (don't forget to set authentication to basic)
            $InvokeSplat.Authentication = 'Basic'
        }
        elseif ($script:NeocitiesAccessToken) {
            # If we had a cached access token, use it
            $InvokeSplat.Headers = @{Authorization = "Bearer $($script:NeocitiesAccessToken)"}
        }
        elseif ($script:NeoCitiesCredential) {
            # If we had a cached credential, use it
            $InvokeSplat.Credential = $script:NeoCitiesCredential
            # and don't forget to set authentication to basic.
            $InvokeSplat.Authentication = 'Basic'
        }

        # If neither an access token nor a credential was provided, we can't do anything.
        if (-not $InvokeSplat.Credential -and -not $InvokeSplat.Headers)
        {
            # so error out.
            Write-Error "No -Credential provided"
            return
        }

        $InvokeSplat.ContentType = $contentType

        # If we were piped in a file
        if ($_ -is [IO.FileInfo]) {
            $file = $_ # set the parameter directly
        }

        # For every file passed in, we need to make a unique request.
        foreach ($fileInfo in $file) {
            # If this is a file, this is easy
            if ($fileInfo -is [IO.FileInfo]) {
                # just get the string representation of the file's bytes and add them to the multipart collection
                $multiparts[$file.Name] = $OutputEncoding.GetString([IO.File]::ReadAllBytes($file))
            }
            # If the file was a path, we need to get the file's contents
            elseif ($fileInfo -is [string] -and (Test-Path $fileInfo)) {
                $multiparts[$fileInfo] = Get-Content -Raw $fileInfo
            }
            # If the file was a dictionary, treat each key as a file name and each value as the file's contents
            elseif ($fileInfo -is [Collections.IDictionary]) {
                foreach ($keyValuePair in $fileInfo.GetEnumerator()) {
                    # If the value is a byte array, convert it to a string
                    if ($keyValuePair.Value -is [byte[]]) {                        
                        $multiparts[$keyValuePair.Key] = $OutputEncoding.GetString($keyValuePair.Value)                    
                    }
                    # If the value is a file, read the file's bytes and convert them to a string
                    elseif ($keyValuePair.Value -is [IO.FileInfo]) {
                        $multiparts[$keyValuePair.Key] = $OutputEncoding.GetString([IO.File]::ReadAllBytes($keyValuePair.Value))
                    }
                    # If the value is a pth to file, read the file's bytes and convert them to a string
                    elseif ($keyValuePair.Value -is [string] -and (Test-Path $keyValuePair.Value)) {
                        $multiparts[$keyValuePair.Key] = Get-Content -Raw $keyValuePair.Value
                    }
                    # last but not least, stringify the value and add it to the collection
                    else
                    {
                        $multiparts[$keyValuePair.Key] = "$($keyValuePair.Value)"
                    }                           
                }
            }
        }
        
    }

    end {
        # Despite the content type being multipart, we can actually only send one part at a time:

        # Any way we slice it, we'll need to POST the data to the API.
        $InvokeSplat.Method = 'POST'


        # For each part we've found
        foreach ($filePart in $multiparts.GetEnumerator()) {
            $InvokeSplat.Body = @(
                # Create a bounary
                "--$boundary"
                # Add the file name and content type to the header
                "Content-Disposition: form-data; name=`"$($filePart.Key)`"; filename=`"$($filePart.Key)`""
                # We're always uploading this a text/plain with neocities, and we need to set the encoding to whatever we passed.
                "Content-Type: text/plain; charset=$($OutputEncoding.WebName)"
                # The bounary MIME data must be followed by a newline
                "`r`n"
                # followed by the file contents
                $filePart.Value
                # followed by an additional boundary
                "--$boundary--"
            ) -join "`r`n" # (all of these pieces are joined by a newline)

            # If -WhatIf was passed, don't actually upload the file, just show the splatted parameters.
            if ($WhatIfPreference) {
                # (Remove the headers and credential from the splatted parameters, so we don't leak any sensitive information)
                $InvokeSplat.Remove('Headers')
                $InvokeSplat.Remove('Credential')
                $InvokeSplat
                continue
            }

            # Invoke-RestMethod with our splatted parameters, and we'll upload the file.
            Invoke-RestMethod @InvokeSplat
        }
    }
}