Attachment.psm1

#!/usr/bin/env pwsh
$ErrorActionPreference = "Stop"


function New-Attachment
{
    <#
        .SYNOPSIS

            Add a new attachment

        .DESCRIPTION


        .OUTPUTS

            When no $Title is provided and the $Manifest array only contains 1
            page metadata, the ``Count`` attribute is faulty. Why? Don't know.

        .EXAMPLE
            Add-ConfluencePage `
                -Host 'confluence.contoso.com' `
                -Space 'TIARA' `
                -Title 'Testitest' `
                -Content @{}
    #>

    Param(
        # confluence instance hostname
        [Parameter(Mandatory)] [string]$Host,
        # name of the Confluence space to publish to
        [Parameter(Mandatory)] [string]$Space,
        # title of page to be published
        [Parameter()] [string]$Name,
        # attachments manifest
        [Parameter(Mandatory, ValueFromPipeline)] 
            [PSCustomObject[]]$Manifest,
        # attachments manifest index, mandatory for ancestor lookup
        [Parameter(Mandatory)] [Collections.Hashtable]$Index,
        # pages manifest
        [Parameter(Mandatory)] 
            [PSCustomObject[]]$PagesManifest,
        # pages manifest index, mandatory for container page lookup
        [Parameter(Mandatory)] [Collections.Hashtable]$PagesIndex,
        # flag on whether to fail hard, or just continue
        [Parameter()] [Switch]$Strict
    )

    Begin
    {
        $pat = Get-PersonalAccessToken $Host
    }

    Process
    {
        If ($Name -And $Manifest[$Index.$Name])
        {
            $Manifest = @(
                $Manifest[$Index.$Name]
            )
        }

        ForEach($attachmentMeta in $Manifest)
        {
            If ($Name -And $attachmentMeta.Name -ne $Name) {continue}


            $containerPageMeta = $PagesManifest[
                $PagesIndex."$($attachmentMeta.ContainerPageTitle)"
            ]

            If (-Not $containerPageMeta)
            {
                throw (
                    "Get-AttachmentMeta: ``$($attachmentMeta.Name)``: " + 
                    "unable to lookup metadata for container page " +
                    "title ``$($attachmentMeta.ContainerPageTitle)``." +
                    "This is fatal."
                )
            }

            If (-Not $containerPageMeta.Id)
            {
                $errMsg = (
                    "Get-AttachmentMeta: ``$($attachmentMeta.Name)``: " + 
                    "container page titled" +
                    "``$($attachmentMeta.ContainerPageTitle)`` " + 
                    "has no id, which means that the page has " +
                    "(presumably) not yet been published."
                )

                If ($Strict) {throw $errMsg}

                Write-Host "$errMsg Continuing nonetheless..."

                $attachmentMeta

                continue
            }
            ElseIf (-Not $attachmentMeta.Ref)
            {
                $errMsg = (
                    "``$($attachmentMeta.Name)``: no reference to local " + 
                    'content for attachment .'
                )

                If ($Strict) {throw $errMsg}

                Write-Host $errMsg

                # not outputting the metadata, since it's invalid anyway

                continue
            }

            ElseIf ($attachmentMeta.Id)
            {

                Write-Debug (
                    "New-Attachment: ``$($attachmentMeta.Name)``: skipping, " +
                    "already published ($($attachmentMeta.Id))"
                )

                $attachmentMeta

                continue
            }

            Else
            {
                Write-Host (
                    "New-Attachment: ``$($attachmentMeta.Name)``: creating"
                )

                Try
                {
                    $rawContent = [IO.File]::ReadAllBytes($attachmentMeta.Ref)

                    $content = [Text.Encoding]::GetEncoding(
                        'ISO-8859-1'
                    ).GetString($rawContent)
                }

                Catch
                {
                    $errMsg = "``New-Attachment: $($attachmentMeta.Name)``: $_"

                    If ($Strict) {throw $errMsg}

                    Write-Host $errMsg

                    continue
                }

                $boundary = [Guid]::NewGuid().ToString()

                $LF = "`r`n";

                $transportBody = ( 
                    "--$boundary",
                    (
                        "Content-Disposition: form-data; name=`"file`"; " +
                        "filename=`"$($attachmentMeta.Name)`""
                    ),
                    "Content-Type: $($attachmentMeta.MimeType)$LF",
                    $content,
                    "--$boundary--$LF" 
                ) -join $LF

                $uri = (
                    "https://${Host}/rest/api/content/" +
                    "$($containerPageMeta.Id)/child/attachment"
                )

                Try
                {
                    Invoke-WebRequest `
                        -Uri $uri `
                        -Method 'Post' `
                        -Headers @{
                            'Authorization' = "Bearer $pat"
                            'X-Atlassian-Token' = 'nocheck'
                        } `
                        -ContentType (
                            "multipart/form-data; boundary=`"$boundary`""
                        ) `
                        -Body $transportBody `
                        -OutVariable rawResponse | Out-Null
                }

                Catch
                {
                    $errMsg = "skipping ``$($attachmentMeta.Name)``: $($_)"

                    If ($Strict)
                    {
                        $_

                        throw $errMsg
                    }

                    Write-Host $errMsg

                    continue
                }

                $response = ($rawResponse.Content | ConvertFrom-JSON)

                $result = $response.results[0]

                $attachmentMeta | Add-Member `
                                      -NotePropertyName 'Id' `
                                      -NotePropertyValue $result.id `
                                      -Force

                $attachmentMeta | Add-Member `
                                      -NotePropertyName 'Version' `
                                      -NotePropertyValue (
                                          $result.version.number
                                      ) `
                                      -Force

                $contentHash = (Get-StringHash $content).Hash

                $attachmentMeta | Add-Member `
                                      -NotePropertyName 'Hash' `
                                      -NotePropertyValue $contentHash `
                                      -Force

                If (
                    ($Title -And $attachmentMeta.Title -eq $Name) -Or 
                    $Manifest.Count -eq 1
                )
                {
                    # TODO: further research mechanism of expanding single item
                    # array pipelines. For now we have to apply the unary
                    # operator, otherwise we get a wrong count on the output
                    ,@($attachmentMeta)

                    break
                }

                Else
                {
                    $attachmentMeta
                }
            }
        }
    }
}


function Update-Attachment
{
    <#
        .SYNOPSIS

            Add a new attachment

        .DESCRIPTION


        .OUTPUTS

            When no $Title is provided and the $Manifest array only contains 1
            page metadata, the ``Count`` attribute is faulty. Why? Don't know.

        .EXAMPLE
            Add-ConfluencePage `
                -Host 'confluence.contoso.com' `
                -Space 'TIARA' `
                -Title 'Testitest' `
                -Content @{}
    #>

    Param(
        # confluence instance hostname
        [Parameter(Mandatory)] [string]$Host,
        # name of the Confluence space to publish to
        [Parameter(Mandatory)] [string]$Space,
        # title of page to be published
        [Parameter()] [string]$Name,
        # attachments manifest
        [Parameter(Mandatory, ValueFromPipeline)] 
            [PSCustomObject[]]$Manifest,
        # attachments manifest index, mandatory for ancestor lookup
        [Parameter(Mandatory)] [Collections.Hashtable]$Index,
        # pages manifest
        [Parameter(Mandatory)] 
            [PSCustomObject[]]$PagesManifest,
        # pages manifest index, mandatory for container page lookup
        [Parameter(Mandatory)] [Collections.Hashtable]$PagesIndex,
        # flag on whether to fail hard, or just continue
        [Parameter()] [Switch]$Strict,
        # flag on whether to force update of attachment, regardless of content
        # changes
        [Parameter()] [Switch]$Force
    )

    Begin
    {
        $pat = Get-PersonalAccessToken $Host
    }

    Process
    {
        If ($Name -And $Manifest[$Index.$Name])
        {
            $Manifest = @(
                $Manifest[$Index.$Name]
            )
        }

        ForEach($attachmentMeta in $Manifest)
        {
            If ($Name -And $attachmentMeta.Name -ne $Name) {continue}

            $containerPageMeta = $PagesManifest[
                $PagesIndex."$($attachmentMeta.ContainerPageTitle)"
            ]

            If (-Not $containerPageMeta)
            {
                throw (
                    "Get-AttachmentMeta: ``$($attachmentMeta.Name)``: " + 
                    "unable to lookup metadata for container page " +
                    "title ``$($attachmentMeta.ContainerPageTitle)``." +
                    "This is fatal."
                )
            }

            ElseIf (-Not $containerPageMeta.Id)
            {
                $errMsg = (
                    "Get-AttachmentMeta: ``$($attachmentMeta.Name)``: " + 
                    "container page titled" +
                    "``$($attachmentMeta.ContainerPageTitle)`` " + 
                    "has no id, which means that the page has " +
                    "(presumably) not yet been published."
                )

                If ($Strict) {throw $errMsg}

                Write-Host "$errMsg Continuing nonetheless..."

                $attachmentMeta

                continue
            }

            ElseIf (-Not $attachmentMeta.Ref)
            {
                $errMsg = (
                    "``$($attachmentMeta.Name)``: no reference to local " + 
                    'content for attachment .'
                )

                If ($Strict) {throw $errMsg}

                Write-Host $errMsg

                # not outputting the metadata, since it's invalid anyway

                continue
            }

            ElseIf (-Not $attachmentMeta.Id)
            {
                $errMsg = (
                    "Update-Attachment: ``$($attachmentMeta.Name)``: unknown " +
                    "attachment id."
                )

                If ($Strict) {throw $errMsg}

                Write-Host "$errMsg Skipping."

                $attachmentMeta

                continue
            }

            ElseIf (-Not $attachmentMeta.Version)
            {
                Write-Host = (
                    "New-Attachment: ``$($attachmentMeta.Name)``: skipping, " +
                    "unknown (current) version"
                )
            }

            Else
            {

                Try
                {
                    $rawContent = [IO.File]::ReadAllBytes($attachmentMeta.Ref)

                    $content = [Text.Encoding]::GetEncoding(
                        'ISO-8859-1'
                    ).GetString($rawContent)
                }

                Catch
                {
                    $errMsg = "``New-Attachment: $($attachmentMeta.Name)``: $_"

                    If ($Strict) {throw $errMsg}

                    Write-Host $errMsg

                    continue
                }

                $version = [Int]$attachmentMeta.Version + 1

                $contentHash = (Get-StringHash $content).Hash

                If (
                    $attachmentMeta.Hash -And 
                    $attachmentMeta.Hash -eq $contentHash -And
                    -Not $Force
                )
                {
                    Write-Debug (
                        "Update-Attachment: ``$($attachmentMeta.Name)``: " +
                        "skipping, no content changes"
                    )

                    $attachmentMeta

                    continue
                }

                Else
                {
                    Write-Host (
                        "Update-Attachment: ``$($attachmentMeta.Name)``: " +
                        "updating"
                    )
                }

                $boundary = [Guid]::NewGuid().ToString()

                $LF = "`r`n";

                $transportBody = ( 
                    "--$boundary",
                    (
                        "Content-Disposition: form-data; name=`"file`"; " +
                        "filename=`"$($attachmentMeta.Name)`""
                    ),
                    "Content-Type: $($attachmentMeta.MimeType)$LF",
                    $content,
                    "--$boundary--$LF" 
                ) -join $LF

                $uri = (
                    "https://${Host}/rest/api/content/" +
                    "$($containerPageMeta.Id)/child/attachment/" +
                    "$($attachmentMeta.Id)/data"
                )

                Try
                {
                    Invoke-WebRequest `
                        -Uri $uri `
                        -Method 'Post' `
                        -Headers @{
                            'Authorization' = "Bearer $pat"
                            'X-Atlassian-Token' = 'nocheck'
                        } `
                        -ContentType (
                            "multipart/form-data; boundary=`"$boundary`""
                        ) `
                        -Body $transportBody | Out-Null
                }

                Catch
                {
                    $errMsg = "skipping ``$($attachmentMeta.Name)``: $($_)"

                    If ($Strict)
                    {
                        $_

                        throw $errMsg
                    }

                    Write-Host $errMsg

                    continue
                }

                # response isn't needed since no field will be updated by the
                # Confluence instance itself

                $attachmentMeta | Add-Member `
                                      -NotePropertyName 'Version' `
                                      -NotePropertyValue $version `
                                      -Force

                $attachmentMeta | Add-Member `
                                      -NotePropertyName 'Hash' `
                                      -NotePropertyValue $contentHash `
                                      -Force

                If (
                    ($Title -And $attachmentMeta.Title -eq $Name) -Or 
                    $Manifest.Count -eq 1
                )
                {
                    # TODO: further research mechanism of expanding single item
                    # array pipelines. For now we have to apply the unary
                    # operator, otherwise we get a wrong count on the output
                    ,@($attachmentMeta)

                    break
                }

                Else
                {
                    $attachmentMeta
                }
            }
        }
    }
}

function Publish-Attachment
{
    Param(
        # confluence instance hostname
        [Parameter(Mandatory)] [string]$Host,
        # name of the Confluence space to publish to
        [Parameter(Mandatory)] [string]$Space,
        # title of page to be published
        [Parameter()] [string]$Name,
        # attachments manifest
        [Parameter(Mandatory,ValueFromPipeline)] 
            [PSCustomObject[]]$Manifest,
        # attachments manifest index, mandatory for ancestor lookup
        [Parameter(Mandatory)] [Collections.Hashtable]$Index,
        # pages manifest
        [Parameter(Mandatory)] 
            [PSCustomObject[]]$PagesManifest,
        # pages manifest index, mandatory for container page lookup
        [Parameter(Mandatory)] [Collections.Hashtable]$PagesIndex,
        # flag on whether to fail hard, or just continue
        [Parameter()] [Switch]$Strict,
         # flag on whether to force update of page, regardless of content
        [Parameter()] [Switch]$Force
   )

    Process
    {
        $result = Update-Attachment `
                      -Host $Host `
                      -Space $Space `
                      -Manifest $Manifest `
                      -Index $Index `
                      -PagesManifest $PagesManifest `
                      -PagesIndex $PagesIndex `
                      -Strict:$Strict `
                      -Force:$Force

        $result = New-Attachment `
                      -Host $Host `
                      -Space $Space `
                      -Manifest $manifest `
                      -Index $Index `
                      -PagesManifest $PagesManifest `
                      -PagesIndex $PagesIndex `
                      -Strict:$Strict
    }

    End
    {
        $result
    }
}