From 496d70360bf5055c08324cf38c266f9574b2d052 Mon Sep 17 00:00:00 2001 From: "Rodweil, Theodor" Date: Mon, 14 Aug 2023 03:38:57 +0200 Subject: [PATCH] refactor: move page meta to common meta module --- src/Attachment.psm1 | 291 ++++++++++++++++++++++++++++ src/Meta.psm1 | 340 +++++++++++++++++++++++++++++++++ src/PSConfluencePublisher.psd1 | 3 +- src/PSConfluencePublisher.psm1 | 156 ++++++++++----- src/Page.psm1 | 68 ++++++- src/PageMeta.psm1 | 132 ------------- 6 files changed, 804 insertions(+), 186 deletions(-) create mode 100755 src/Attachment.psm1 create mode 100755 src/Meta.psm1 delete mode 100755 src/PageMeta.psm1 diff --git a/src/Attachment.psm1 b/src/Attachment.psm1 new file mode 100755 index 0000000..468f1b4 --- /dev/null +++ b/src/Attachment.psm1 @@ -0,0 +1,291 @@ +#!/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: ``$($attacmentMeta.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) + + $attachmentMeta | Add-Member ` + -NotePropertyName 'Id' ` + -NotePropertyValue $response.id ` + -Force + + $attachmentMeta | Add-Member ` + -NotePropertyName 'Version' ` + -NotePropertyValue ( + $response.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 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 = New-Attachment ` + -Host $Host ` + -Space $Space ` + -Manifest $Manifest ` + -Index $Index ` + -PagesManifest $PagesManifest ` + -PagesIndex $PagesIndex ` + -Strict:$Strict + } + + End + { + $result + } +} + diff --git a/src/Meta.psm1 b/src/Meta.psm1 new file mode 100755 index 0000000..285c74e --- /dev/null +++ b/src/Meta.psm1 @@ -0,0 +1,340 @@ +$ErrorActionPreference = "Stop" + + +function Get-PageMeta +{ + <# + .SYNOPSIS + Get a Confluence page id + + .DESCRIPTION + First, tries to retrieve from local page id index (cache) through + the local alias. If no cache hit, then polls the Confluence + instance host for the id by providing a space key and page title. + + .EXAMPLE + Get-PageMeta ` + -Host 'confluence.contoso.com' ` + -Title 'Testitest' ` + -Space 'TIARA' ` + -CacheIndexFile 'confluence-page-cache.json' + #> + Param( + # Confluence instance hostname + [Parameter(Mandatory)] [string]$Host, + # Page title + [Parameter()] [string]$Title, + # Confluence space id + [Parameter(Mandatory)] [string]$Space, + # pages manifest + [Parameter(Mandatory, ValueFromPipeline)] [Array]$Manifest, + # page metadata index for faster lookup of single page + [Parameter()] [Collections.Hashtable]$Index, + # force to get metadata from remote + [Parameter()] [Switch]$Force, + # throw an exception on error + [Parameter()] [Switch]$Strict + ) + + Process + { + If ($Title -And $Index -And $Manifest[$Index.$Title].Id) + { + $Manifest[$Index.$Title] + + return + } + + ForEach ($pageMeta in $Manifest) + { + If ($Title -And $pageMeta.Title -ne $Title) {continue} + + If ($pageMeta.Id -And -Not $Force) + { + Write-Debug ( + "Get-PageMeta: ``$($pageMeta.Title)``: " + + "using locally cached metadata ($($pageMeta.Id))" + ) + + $pageMeta + } + + Else + { + $escapedTitle = [Uri]::EscapeDataString($pageMeta.Title) + + $query = ( + "title=${escapedTitle}&spaceKey=${Space}&expand=version" + ) + + Invoke-WebRequest ` + -Uri "https://${Host}/rest/api/content?$query" ` + -Method 'Get' ` + -Headers @{ + 'Authorization' = 'Bearer ' + + $(Get-PersonalAccessToken $Host) + } ` + -OutVariable response | Out-Null + + $results = ($response.Content | ConvertFrom-JSON).results + + If ($results.Count -gt 1) + { + $errMsg = "error: more than one result for query: $query" + + If ($Strict) {throw $errMsg} + + Write-Host $errMsg + + $pageMeta + + continue + } + + ElseIf ($results.Count -eq 1) + { + Write-Debug ( + "Get-PageMetadata: ``$($pageMeta.Title)``: " + + "updating metadata through remote ($($results[0].id))" + ) + + $pageMeta | Add-Member ` + -NotePropertyName Id ` + -NotePropertyValue $results[0].id ` + -Force + + $pageMeta | Add-Member ` + -NotePropertyName 'Version' ` + -NotePropertyValue ` + $results[0].version.number ` + -Force + } + + Else + { + Write-Debug ( + "Get-PageMetadata: ``$($pageMeta.Title)``: " + + "no remote, using (partial) local" + ) + + If ($pageMeta.Version) + { + $pageMeta.PSObject.Properties.Remove('Version') + } + + If ($pageMeta.Id) + { + $pageMeta.PSObject.Properties.Remove('Id') + } + } + + If (-Not $pageMeta.Hash) + { + $content = Get-Content $pageMeta.Ref | Out-String + + $hash = (Get-StringHash $content).Hash + + $pageMeta | Add-Member ` + -NotePropertyName 'Hash' ` + -NotePropertyValue $hash ` + -Force + } + + $pageMeta + } + } + } +} + + +function Get-AttachmentMeta +{ + <# + .SYNOPSIS + Get a Confluence page id + + .DESCRIPTION + First, tries to retrieve from local page id index (cache) through + the local alias. If no cache hit, then polls the Confluence + instance host for the id by providing a space key and page title. + + .EXAMPLE + Get-PageMeta ` + -Host 'confluence.contoso.com' ` + -Title 'Testitest' ` + -Space 'TIARA' ` + -CacheIndexFile 'confluence-page-cache.json' + #> + Param( + # Confluence instance hostname + [Parameter(Mandatory)] [string]$Host, + # Confluence space id + [Parameter(Mandatory)] [string]$Space, + # Attachment name + [Parameter()] [string]$Name, + # attachments manifest + [Parameter(Mandatory, ValueFromPipeline)] [Array]$Manifest, + # page metadata index for faster lookup of single page + [Parameter(Mandatory)] [Collections.Hashtable]$Index, + # pages manifest + [Parameter(Mandatory)] [Array]$PagesManifest, + # page metadata index for faster lookup of single page + [Parameter()] [Collections.Hashtable]$PagesIndex, + # force to get metadata from remote + [Parameter()] [Switch]$Force, + # throw an exception on error + [Parameter()] [Switch]$Strict + ) + + Begin + { + $pat = Get-PersonalAccessToken $Host + } + + Process + { + If ($Name -And $Index -And $Manifest[$Index.$Name].Id) + { + $Manifest[$Index.$Name] + + return + } + + 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 + } + + If ($attachmentMeta.Id -And -Not $Force) + { + Write-Debug ( + "Get-AttachmentMeta: ``$($attachmentMeta.Name)``: " + + "using locally cached metadata ($($attachmentMeta.Id))" + ) + + $attachmentMeta + } + + Else + { + $escapedName = [Uri]::EscapeDataString($attachmentMeta.Name) + + $query = "filename=${escapedName}&expand=version" + + $uri = ( + "https://${Host}/rest/api/content/" + + "$($containerPageMeta.Id)/child/attachment?$query" + ) + + Invoke-WebRequest ` + -Uri $uri ` + -Method 'Get' ` + -Headers @{ + 'Authorization' = "Bearer $pat" + } ` + -OutVariable response | Out-Null + + $results = ($response.Content | ConvertFrom-JSON).results + + If ($results.Count -gt 1) + { + $errMsg = ( + "Get-AttachmentMeta: ``$($attachmentMeta.Name)``: " + + "error: more than one result for query: $query" + ) + + If ($Strict) {throw $errMsg} + + Write-Host $errMsg + + $attachmentMeta + + continue + } + + ElseIf ($results.Count -eq 1) + { + Write-Debug ( + "Get-AttachmentMeta: ``$($attachmentMeta.Name)``: " + + "updating metadata through remote ($($results[0].id))" + ) + + $attachmentMeta | Add-Member ` + -NotePropertyName Id ` + -NotePropertyValue $results[0].id ` + -Force + + $attachmentMeta | Add-Member ` + -NotePropertyName 'Version' ` + -NotePropertyValue ` + $results[0].version.number ` + -Force + } + + Else + { + Write-Debug ( + "Get-AttachmentMetadata: ``$($attachmentMeta.Name)``" + + ": no remote, using (partial) local" + ) + + If ($attachmentMeta.Version) + { + $attachmentMeta.PSObject.Properties.Remove('Version') + } + + If ($attachmentMeta.Id) + { + $attachmentMeta.PSObject.Properties.Remove('Id') + } + } + + If (-Not $attachmentMeta.Hash) + { + $content = Get-Content $attachmentMeta.Ref | Out-String + + $hash = (Get-StringHash $content).Hash + + $attachmentMeta | Add-Member ` + -NotePropertyName 'Hash' ` + -NotePropertyValue $hash ` + -Force + } + + $attachmentMeta + } + } + } +} diff --git a/src/PSConfluencePublisher.psd1 b/src/PSConfluencePublisher.psd1 index f105fb4..46d2d4b 100755 --- a/src/PSConfluencePublisher.psd1 +++ b/src/PSConfluencePublisher.psd1 @@ -64,7 +64,8 @@ Description = 'External Confluence publisher for xconfluencebuilder' 'Connection.psm1', 'Manifest.psm1', 'Page.psm1', - 'PageMeta.psm1', + 'Meta.psm1', + 'Attachment.psm1', 'StringHelper.psm1' ) diff --git a/src/PSConfluencePublisher.psm1 b/src/PSConfluencePublisher.psm1 index 9d36105..2972d44 100755 --- a/src/PSConfluencePublisher.psm1 +++ b/src/PSConfluencePublisher.psm1 @@ -1,42 +1,17 @@ -#!/usr/bin/env pwsh -<# - .SYNOPSIS - PowerShell Publisher for sphinxcontrib.confluencebuilder - - .DESCRIPTION - - - support for ancestral pages and containered attachments - - creates new pages if they don't exist - - updates existing pages and attachments if checksum mismatches - - .EXAMPLE - - Import-Module (Join-Path 'vendor' 'tiara.rodney' - 'PSConfluencePublisher' - 'PSConfluencePublisher' - 'PSConfluencePublisher.psd1') - - Register-PersonalAccessToken ` - -Host 'confluence.contoso.com' ` - -Token '123456789123456789' - - Test-Connection confluence.contoso.com - - Publish-All ` - -Url 'https://confluence.contoso.com/display/TIARA/Testitest' ` - -DumpIndex build/docs/confluence.out/data.json - - .NOTES - - tested with PowerShell Core (PSVersion 7.3.6) - - tested with PowerShell Desktop (PSVersion 5.1.19041.3031) -#> $ErrorActionPreference = "Stop" function Initialize-Manifest { <# + .SYNOPSIS + Initialize a manifest (in-memory) + .DESCRIPTION + This function initializes a manifest by loading a serialized + manifest from the filesystem, generating indexes and sorting the + pages manifest, so that the ancestral relation defines the order in + which pages will be published. #> Param( # path of manifest to load @@ -50,27 +25,40 @@ function Initialize-Manifest Process { - Write-Debug 'loading manifest...' + Write-Debug 'Initialize-Manifest: loading manifest...' $manifest = Get-Manifest $literalPath - Write-Debug 'creating pages manifest index...' + Write-Debug 'Initialize-Manifest: creating pages manifest index...' $pagesManifestIndex = New-PagesManifestIndex -Manifest $manifest.Pages - Write-Debug 'creating ancestral page generation cache...' + Write-Debug ( + 'Initialize-Manifest: creating ancestral page generation cache...' + ) $ancestralGenerationCache = New-AncestralPageGenerationCache ` -Manifest $manifest.Pages ` -Index $pagesManifestIndex - Write-Debug 'sorting pages manifest...' + Write-Debug 'Initialize-Manifest: sorting pages manifest...' Optimize-PagesManifest ` -Manifest $manifest.Pages ` -Lo 0 ` -Hi ($manifest.Pages.Count - 1) ` -GenerationCache $ancestralGenerationCache | Out-Null + + Write-Debug 'Initialize-Manifest: recreating pages manifest index...' + + $pagesManifestIndex = New-PagesManifestIndex -Manifest $manifest.Pages + + Write-Debug ( + 'Initialize-Manifest: creating attachments manifest index...' + ) + + $attachmentsManifestIndex = New-AttachmentsManifestIndex ` + -Manifest $manifest.Attachments } End @@ -79,10 +67,8 @@ function Initialize-Manifest 'Path' = $literalPath 'Manifest' = $manifest 'Index' = @{ - 'Pages' = New-PagesManifestIndex ` - -Manifest $manifest.Pages - 'Attachments' = New-AttachmentsManifestIndex ` - -Manifest $manifest.Attachments + 'Pages' = $pagesManifestIndex + 'Attachments' = $attachmentsManifestIndex } } } @@ -91,9 +77,25 @@ function Initialize-Manifest function Initialize-Connection { + <# + .SYNOPSIS + initialize a connection to a Confluence instance + + .DESCRIPTION + This function registers a Personal Access Token (locally) and checks + connectivity to a Confluence instance. It also verifies, that the + Personal Access Tokens authenticates. + + .NOTES + TODO: extend verification to also verify that write access to the + provided space is granted. + #> Param( + # hostname (or IP address) of Confluence instance [Parameter(Mandatory)] [String]$Host, + # id of Confluence space [Parameter(Mandatory)] [String]$Space, + # personal access token [Parameter(Mandatory)] [String]$PersonalAccessToken ) @@ -118,6 +120,23 @@ function Initialize-Connection function Publish-Pages { + <# + .SYNOPSIS + Publish pages to Confluence instance + + .DESCRIPTION + This function publishes all (or one) pages as defined in the pages + manifest. + + Since pipelining is supported within the low-level functions, this + function is basically just a wrapper. + + .NOTE + TODO: Investigate on how we can pass-through the manifest as to + retain pipeline functionality throughout. Currently it is broken, + since the manifest isn't passed as a pieline input object from the + top (which is this function). + #> Param( # connection object created through Initialize-Connection [Parameter(Mandatory)] [Collections.Hashtable]$Connection, @@ -133,12 +152,55 @@ function Publish-Pages Process { - $Manifest.Manifest.Pages | Publish-Page ` - -Host $Connection.Host ` - -Space $Connection.Space ` - -Title $Title ` - -Index $Manifest.Index.Pages ` - -Strict:$Strict ` - -Force:$Force | Out-Null + Publish-Page ` + -Host $Connection.Host ` + -Space $Connection.Space ` + -Title $Title ` + -Index $Manifest.Index.Pages ` + -Manifest $Manifest.Manifest.Pages ` + -Strict:$Strict ` + -Force:$Force | Out-Null + } +} + + +function Publish-Attachments +{ + <# + .SYNOPSIS + Publish attachments to Confluence instance + + .DESCRIPTION + This function publishes all (or one) attachments as defined in the + attachments manifest. + + Since pipelining is supported within the low-level functions, this + function is basically just a wrapper. + #> + Param( + # connection object created through Initialize-Connection + [Parameter(Mandatory)] [Collections.Hashtable]$Connection, + # manifest object created through Initialize-Manifest + [Parameter(Mandatory)] [PSCustomObject]$Manifest, + # + [Parameter()] [Switch]$Strict, + # + [Parameter()] [Switch]$Force, + # name of attachment to be published + [Parameter()] [String]$Name + ) + + Process + { + Publish-Attachment ` + -Host $Connection.Host ` + -Space $Connection.Space ` + -Name $Name ` + -Manifest $Manifest.Manifest.Attachments ` + -Index $Manifest.Index.Attachments ` + -PagesManifest $Manifest.Manifest.Pages ` + -PagesIndex $Manifest.Index.Pages ` + -Strict:$Strict ` + -Force:$Force | Out-Null } } diff --git a/src/Page.psm1 b/src/Page.psm1 index e430ebf..0df3019 100755 --- a/src/Page.psm1 +++ b/src/Page.psm1 @@ -38,7 +38,7 @@ function New-Page # title of page to be published [Parameter()] [string]$Title, # pages manifest - [Parameter(Mandatory)] + [Parameter(Mandatory,ValueFromPipeline)] [PSCustomObject[]]$Manifest, # pages manifest index, mandatory for ancestor lookup [Parameter(Mandatory)] [Collections.Hashtable]$Index, @@ -123,7 +123,34 @@ function New-Page 'representation' = 'storage' } } - } | ConvertTo-JSON -Depth 5 + } + + If ($pageMeta.AncestorTitle) + { + $ancestorPageMeta = $Manifest[ + $Index."$($pageMeta.AncestorTitle)" + ] + + If (-Not $ancestorPageMeta) + { + Throw ( + "ancestor (``$($ancestorPageMeta.Title)``) of " + + "``$($pageMeta.Title)`` does not have an id. " + + "This indicates, that the ancestor has not been " + + "published and therefore the pages manifest may " + + "not be in the correct order." + ) + } + + $transportBody.ancestors = @( + @{'id' = $ancestorPageMeta.Id} + ) + } + + $rawTransportBody = ( + $transportBody | ConvertTo-JSON ` + -WarningAction 'SilentlyContinue' + ) Try { @@ -134,7 +161,7 @@ function New-Page 'Authorization' = "Bearer $pat" } ` -ContentType "application/json" ` - -Body $transportBody ` + -Body $rawTransportBody ` -OutVariable rawResponse | Out-Null } @@ -277,6 +304,8 @@ function Update-Page Write-Host $errMsg + $pageMeta + continue } @@ -351,7 +380,34 @@ function Update-Page 'version' = @{ 'number' = $version } - } | ConvertTo-JSON -WarningAction 'SilentlyContinue' + } + + If ($pageMeta.AncestorTitle) + { + $ancestorPageMeta = $Manifest[ + $Index."$($pageMeta.AncestorTitle)" + ] + + If (-Not $ancestorPageMeta) + { + Throw ( + "ancestor (``$($ancestorPageMeta.Title)``) of " + + "``$($pageMeta.Title)`` does not have an id. " + + "This indicates, that the ancestor has not been " + + "published and therefore the pages manifest may " + + "not be in the correct order." + ) + } + + $transportBody.ancestors = @( + @{'id' = $ancestorPageMeta.Id} + ) + } + + $rawTransportBody = ( + $transportBody | ConvertTo-JSON ` + -WarningAction 'SilentlyContinue' + ) Try { @@ -363,7 +419,7 @@ function Update-Page 'Authorization' = "Bearer $pat" } ` -ContentType "application/json" ` - -Body $transportBody ` + -Body $rawTransportBody ` -OutVariable rawResponse | Out-Null } @@ -430,7 +486,7 @@ function Publish-Page # title of page to be published [Parameter()] [string]$Title, # pages manifest - [Parameter(Mandatory, ValueFromPipeline)] + [Parameter(Mandatory)] [PSCustomObject[]]$Manifest, # pages manifest index, mandatory for ancestor lookup [Parameter(Mandatory)] [Collections.Hashtable]$Index, diff --git a/src/PageMeta.psm1 b/src/PageMeta.psm1 deleted file mode 100755 index 9927c68..0000000 --- a/src/PageMeta.psm1 +++ /dev/null @@ -1,132 +0,0 @@ - -$ErrorActionPreference = "Stop" - - -function Get-PageMeta -{ - <# - .SYNOPSIS - Get a Confluence page id - - .DESCRIPTION - First, tries to retrieve from local page id index (cache) through - the local alias. If no cache hit, then polls the Confluence - instance host for the id by providing a space key and page title. - - .EXAMPLE - Get-PageMeta ` - -Host 'confluence.contoso.com' ` - -Title 'Testitest' ` - -Space 'TIARA' ` - -CacheIndexFile 'confluence-page-cache.json' - #> - Param( - # Confluence instance hostname - [Parameter(Mandatory)] [string] $Host, - # Page title - [Parameter()] [string] $Title, - # Confluence space id - [Parameter(Mandatory)] [string] $Space, - # pages manifest - [Parameter(Mandatory, ValueFromPipeline)] [Array] $Manifest, - # page metadata index for faster lookup of single page - [Parameter()] [Collections.Hashtable] $Index, - # force to get metadata from remote - [Parameter()] [Switch] $Force = $false, - # throw an exception on error - [Parameter()] [Switch] $Strict = $true - ) - - Process - { - If ($Title -And $Index -And $Manifest[$Index.$Title].Id) - { - $Manifest[$Index.$Title] - - return - } - - ForEach ($pageMeta in $Manifest) - { - If ($Title -And $pageMeta.Title -ne $Title) {continue} - - If ($pageMeta.Id -And -Not $Force) - { - Write-Debug "local (cache): $($pageMeta.Title) ($($pageMeta.Id))" - - $pageMeta - } - - Else - { - $escapedTitle = [Uri]::EscapeDataString($pageMeta.Title) - - $query = ( - "title=${escapedTitle}&spaceKey=${Space}&expand=version" - ) - - Invoke-WebRequest ` - -Uri "https://${Host}/rest/api/content?$query" ` - -Method 'Get' ` - -Headers @{ - 'Authorization' = 'Bearer ' + - $(Get-PersonalAccessToken $Host) - } ` - -OutVariable response | Out-Null - - $results = ($response.Content | ConvertFrom-JSON).results - - If ($results.Count -gt 1) - { - $errMsg = "error: more than one result for query: $query" - - If ($Strict) {throw $errMsg} - - Write-Host $errMsg - - $pageMeta - - continue - } - - ElseIf ($results.Count -eq 1) - { - Write-Debug ( - "Get-PageMetadata: ``$($pageMeta.Title)``: " + - "updating metadata through remote ($($results[0].id))" - ) - - $pageMeta | Add-Member ` - -NotePropertyName Id ` - -NotePropertyValue $results[0].id ` - -Force - - $pageMeta | Add-Member ` - -NotePropertyName 'Version' ` - -NotePropertyValue ` - $results[0].version.number ` - -Force - } - - Else - { - Write-Debug "local: $($pageMeta.Title) (no remote)" - } - - If (-Not $pageMeta.Hash) - { - $content = Get-Content $pageMeta.Ref | Out-String - - $hash = (Get-StringHash $content).Hash - - $pageMeta | Add-Member ` - -NotePropertyName 'Hash' ` - -NotePropertyValue $hash ` - -Force - } - - $pageMeta - } - } - } -}