refactor: adapt to nuspec requirement for gallery publishing
This commit is contained in:
parent
2019f8c959
commit
69c2684ee7
29 changed files with 84 additions and 20 deletions
82
tools/Connection.Tests.ps1
Executable file
82
tools/Connection.Tests.ps1
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
BeforeAll {
|
||||
Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1')
|
||||
}
|
||||
|
||||
|
||||
Describe 'Test-Connection' `
|
||||
{
|
||||
Context 'default' {
|
||||
BeforeAll `
|
||||
{
|
||||
Mock -ModuleName 'Connection' Get-PersonalAccessToken {
|
||||
'01234567890123456789'
|
||||
}
|
||||
}
|
||||
|
||||
It 'throws no exception' {
|
||||
Mock -ModuleName 'Connection' Invoke-WebRequest {
|
||||
@{
|
||||
'Content' = "{'type': 'known'}"
|
||||
'StatusCode' = 200
|
||||
}
|
||||
}
|
||||
|
||||
Test-Connection -Host 'confluence.contoso.com'
|
||||
|
||||
Should -Invoke -CommandName 'Get-PersonalAccessToken' `
|
||||
-ModuleName 'Connection' ` `
|
||||
-Exact `
|
||||
-Times 1
|
||||
|
||||
Should -Invoke -CommandName 'Invoke-WebRequest' `
|
||||
-ModuleName 'Connection' ` `
|
||||
-Exact `
|
||||
-Times 1
|
||||
}
|
||||
|
||||
It 'detects anonymous authentication' {
|
||||
Mock -ModuleName 'Connection' Invoke-WebRequest {
|
||||
@{
|
||||
'Content' = "{'type': 'anonymous'}"
|
||||
'StatusCode' = 200
|
||||
}
|
||||
}
|
||||
|
||||
{Test-Connection -Host 'confluence.contoso.com'} | Should -Throw
|
||||
|
||||
Should -Invoke -CommandName 'Get-PersonalAccessToken' `
|
||||
-ModuleName 'Connection' ` `
|
||||
-Exact `
|
||||
-Times 1
|
||||
|
||||
Should -Invoke -CommandName 'Invoke-WebRequest' `
|
||||
-ModuleName 'Connection' ` `
|
||||
-Exact `
|
||||
-Times 1
|
||||
}
|
||||
|
||||
It 'detects non 200 status codes' {
|
||||
Mock -ModuleName 'Connection' Invoke-WebRequest {
|
||||
@{
|
||||
'Content' = "{'type': 'anonymous'}"
|
||||
'StatusCode' = 500
|
||||
}
|
||||
}
|
||||
|
||||
{Test-Connection -Host 'confluence.contoso.com'} | Should -Throw
|
||||
|
||||
Should -Invoke -CommandName 'Get-PersonalAccessToken' `
|
||||
-ModuleName 'Connection' ` `
|
||||
-Exact `
|
||||
-Times 1
|
||||
|
||||
Should -Invoke -CommandName 'Invoke-WebRequest' `
|
||||
-ModuleName 'Connection' ` `
|
||||
-Exact `
|
||||
-Times 1
|
||||
}
|
||||
}
|
||||
}
|
||||
54
tools/Connection.psm1
Executable file
54
tools/Connection.psm1
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
|
||||
function Test-Connection
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test the connectivity to a Confluence instance.
|
||||
|
||||
.DESCRIPTION
|
||||
Just making an arbitrary authenticated HTTP request and making sure
|
||||
that we're getting a 2xx status code back. This way we make sure
|
||||
that network connectivity is fine, and that the PAT is valid.
|
||||
|
||||
It is required to register a PAT through
|
||||
``Register-PersonalAccessToken`` beforehand.
|
||||
|
||||
.EXAMPLE
|
||||
Test-Connection confluence.contoso.com
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory, Position = 0)] [string] $Host
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
# Screw Invoke-RestMethod, how am i supposed to get a non 4xx status
|
||||
# code? Catch a non-existent exception 🤷♀️????
|
||||
Invoke-WebRequest `
|
||||
-Uri "https://${Host}/rest/api/user/current" `
|
||||
-Method 'Get' `
|
||||
-Headers @{
|
||||
'Authorization' = "Bearer $(Get-PersonalAccessToken $Host)"
|
||||
} `
|
||||
-OutVariable response
|
||||
|
||||
if(($response.Content | ConvertFrom-JSON).type -ne "known")
|
||||
{
|
||||
throw "personal access token for host '$Host' does not " +
|
||||
"authenticate."
|
||||
}
|
||||
|
||||
if ($response.StatusCode -eq 200)
|
||||
{
|
||||
Write-Host "Verified connectivity ($Host)."
|
||||
}
|
||||
else
|
||||
{
|
||||
throw "received status code other than 200 " +
|
||||
"($($response.StatusCode))"
|
||||
}
|
||||
}
|
||||
}
|
||||
415
tools/Manifest.Tests.ps1
Executable file
415
tools/Manifest.Tests.ps1
Executable file
|
|
@ -0,0 +1,415 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
BeforeAll {
|
||||
Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1')
|
||||
}
|
||||
|
||||
|
||||
AfterAll {
|
||||
|
||||
}
|
||||
|
||||
|
||||
Describe 'Get-Manifest' `
|
||||
{
|
||||
Context 'Parameterized' `
|
||||
{
|
||||
It 'can successfully validate against the schema' `
|
||||
{
|
||||
InModuleScope Manifest `
|
||||
{
|
||||
Mock Get-Content {
|
||||
return '{"pages":[], "attachments": []}'
|
||||
}
|
||||
|
||||
#mocking Get-Content, therefore file name can be bogus
|
||||
Get-Manifest 'foobar.x'
|
||||
}
|
||||
}
|
||||
|
||||
It 'throws on schema mismatch' `
|
||||
{
|
||||
InModuleScope Manifest `
|
||||
{
|
||||
Mock Get-Content {
|
||||
return '{"pagges":[], "attsdachments": []}'
|
||||
}
|
||||
|
||||
#mocking Get-Content, therefore file name can be bogus
|
||||
{Get-Manifest 'foobar.x'} | Should -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'Set-Manifest' `
|
||||
{
|
||||
Context 'noBackup' `
|
||||
{
|
||||
It 'can successfully validate against the schema' `
|
||||
{
|
||||
InModuleScope Manifest `
|
||||
{
|
||||
$mockManifest = @{
|
||||
'pages' = @()
|
||||
'attachments' = @()
|
||||
}
|
||||
|
||||
Mock Set-Content {
|
||||
Should -Invoke -CommandName 'Set-Content' -Exactly -Times 1
|
||||
|
||||
$Path | Should -Be 'foobar.x'
|
||||
|
||||
$Value | Should -Be ($mockManifest | ConvertTo-JSON)
|
||||
}
|
||||
|
||||
#mocking Get-Content, therefore file name can be bogus
|
||||
Set-Manifest `
|
||||
-Manifest $mockManifest `
|
||||
-File 'foobar.x'
|
||||
}
|
||||
}
|
||||
|
||||
It 'declines setting invalid schema' `
|
||||
{
|
||||
InModuleScope Manifest `
|
||||
{
|
||||
$mockManifest = @{
|
||||
'pagges' = @()
|
||||
'attachments' = @()
|
||||
}
|
||||
|
||||
#mocking Get-Content, therefore file name can be bogus
|
||||
{
|
||||
Set-Manifest `
|
||||
-Manifest $mockManifest `
|
||||
-File 'foobar.x'
|
||||
} | Should -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Backup' `
|
||||
{
|
||||
It 'creates a backup when it should' `
|
||||
{
|
||||
InModuleScope Manifest `
|
||||
{
|
||||
$mockManifest = @{
|
||||
'pages' = @()
|
||||
'attachments' = @()
|
||||
}
|
||||
|
||||
Mock Set-Content {
|
||||
#FIXME: the scope is completely wrong
|
||||
Should -Invoke -CommandName 'Set-Content' -Exactly -Times 1
|
||||
}
|
||||
|
||||
Mock Copy-Item {
|
||||
#FIXME: the scope is completely wrong
|
||||
Should -Invoke -CommandName 'Copy-Item' -Exactly -Times 1 `
|
||||
|
||||
$Path | Should -Be 'foobar.x'
|
||||
|
||||
$Destination | Should -Be 'foobar.x.bck'
|
||||
}
|
||||
|
||||
#mocking Get-Content, therefore file name can be bogus
|
||||
Set-Manifest `
|
||||
-Manifest $mockManifest `
|
||||
-File 'foobar.x' `
|
||||
-Backup $true
|
||||
}
|
||||
}
|
||||
|
||||
It 'handles paths outside of the current working directory correctly' `
|
||||
{
|
||||
InModuleScope Manifest `
|
||||
{
|
||||
$mockManifest = @{
|
||||
'pages' = @()
|
||||
'attachments' = @()
|
||||
}
|
||||
|
||||
Mock Set-Content {
|
||||
#FIXME: the scope is completely wrong
|
||||
Should -Invoke -CommandName 'Set-Content' -Exactly -Times 1
|
||||
}
|
||||
|
||||
Mock Copy-Item {
|
||||
#FIXME: the scope is completely wrong
|
||||
Should -Invoke -CommandName 'Copy-Item' -Exactly -Times 1 `
|
||||
|
||||
$Path | Should -Be 'foo/bar/foobar.x'
|
||||
|
||||
$Destination | Should -Be 'foo/bar/foobar.x.bck'
|
||||
}
|
||||
|
||||
#mocking Get-Content, therefore file name can be bogus
|
||||
Set-Manifest `
|
||||
-Manifest $mockManifest `
|
||||
-File 'foo/bar/foobar.x' `
|
||||
-Backup $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'New-PagesManifestIndex' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$mockManifest = @(
|
||||
@{
|
||||
'Title' = 'foobar0'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar1'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
It 'from pipeline' `
|
||||
{
|
||||
$index = New-PagesManifestIndex -Manifest $mockManifest
|
||||
|
||||
$index.foobar0 | Should -Be 0
|
||||
|
||||
$index.foobar1 | Should -Be 1
|
||||
}
|
||||
|
||||
It 'from pipeline' `
|
||||
{
|
||||
$index = ,$mockManifest | New-PagesManifestIndex
|
||||
|
||||
$index.foobar0 | Should -Be 0
|
||||
|
||||
$index.foobar1 | Should -Be 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'New-AttachmentsManifestIndex' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$mockManifest = @(
|
||||
@{
|
||||
'ContainerPageTitle' = 'foobar0'
|
||||
'Name' = 'attachment0'
|
||||
},
|
||||
@{
|
||||
'ContainerPageTitle' = 'foobar1'
|
||||
'Name' = 'attachment1'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
It 'from parameter' `
|
||||
{
|
||||
$index = New-AttachmentsManifestIndex -Manifest $mockManifest
|
||||
|
||||
$index."foobar0:attachment0" | Should -Be 0
|
||||
|
||||
$index."foobar1:attachment1" | Should -Be 1
|
||||
}
|
||||
|
||||
It 'from pipeline' `
|
||||
{
|
||||
$index = ,$mockManifest | New-AttachmentsManifestIndex
|
||||
|
||||
$index."foobar0:attachment0" | Should -Be 0
|
||||
|
||||
$index."foobar1:attachment1" | Should -Be 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'Get-AncestralPageGenerationCache' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$mockManifest = @(
|
||||
@{
|
||||
'Title' = 'foobar0'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar1'
|
||||
'AncestorTitle' = 'foobar0'
|
||||
}
|
||||
)
|
||||
|
||||
$mockIndex = @{
|
||||
'foobar0' = 0
|
||||
'foobar1' = 1
|
||||
}
|
||||
}
|
||||
|
||||
It 'selects single operation by title' `
|
||||
{
|
||||
$result = New-AncestralPageGenerationCache `
|
||||
-Title 'foobar0' `
|
||||
-Manifest $mockManifest `
|
||||
-Index $mockIndex
|
||||
|
||||
$result.Count | Should -Be 1
|
||||
|
||||
$result.foobar0 | Should -Be 0
|
||||
}
|
||||
|
||||
It 'automatically builds index' `
|
||||
{
|
||||
$result = New-AncestralPageGenerationCache `
|
||||
-Title 'foobar0' `
|
||||
-Manifest $mockManifest `
|
||||
|
||||
$result.Count | Should -Be 1
|
||||
|
||||
$result.foobar0 | Should -Be 0
|
||||
}
|
||||
|
||||
It 'accepts a pipeline' `
|
||||
{
|
||||
$result = New-AncestralPageGenerationCache `
|
||||
-Manifest $mockManifest `
|
||||
-Index $mockIndex
|
||||
|
||||
$result.Count | Should -Be 2
|
||||
|
||||
$result.foobar0 | Should -Be 0
|
||||
|
||||
$result.foobar1 | Should -Be 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Context 'more complex' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$mockManifest = @(
|
||||
@{
|
||||
'Title' = 'foobar0'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar4'
|
||||
'AncestorTitle' = 'foobar3'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar1'
|
||||
'AncestorTitle' = 'foobar0'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar3'
|
||||
'AncestorTitle' = 'foobar2'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar2'
|
||||
'AncestorTitle' = 'foobar1'
|
||||
}
|
||||
)
|
||||
|
||||
$mockIndex = @{
|
||||
'foobar0' = 0
|
||||
'foobar1' = 2
|
||||
'foobar2' = 4
|
||||
'foobar3' = 3
|
||||
'foobar4' = 1
|
||||
}
|
||||
}
|
||||
|
||||
It 'uses index' `
|
||||
{
|
||||
$result = New-AncestralPageGenerationCache `
|
||||
-Manifest $mockManifest `
|
||||
-Index $mockIndex
|
||||
|
||||
$result.foobar0 | Should -Be 0
|
||||
|
||||
$result.foobar1 | Should -Be 1
|
||||
|
||||
$result.foobar2 | Should -Be 2
|
||||
|
||||
$result.foobar3 | Should -Be 3
|
||||
|
||||
$result.foobar4 | Should -Be 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'Optimize-PagesManifest' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$mockManifest = @(
|
||||
@{
|
||||
'Title' = 'foobar0'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar4'
|
||||
'AncestorTitle' = 'foobar3'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar1'
|
||||
'AncestorTitle' = 'foobar0'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar3'
|
||||
'AncestorTitle' = 'foobar2'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar5'
|
||||
'AncestorTitle' = 'foobar2'
|
||||
},
|
||||
@{
|
||||
'Title' = 'foobar2'
|
||||
'AncestorTitle' = 'foobar1'
|
||||
}
|
||||
)
|
||||
|
||||
$mockGenerationCache = @{
|
||||
'foobar0' = 0
|
||||
'foobar1' = 1
|
||||
'foobar2' = 2
|
||||
'foobar3' = 3
|
||||
'foobar4' = 4
|
||||
'foobar5' = 3
|
||||
}
|
||||
}
|
||||
|
||||
It 'from parameter' `
|
||||
{
|
||||
Optimize-PagesManifest `
|
||||
-Manifest $mockManifest `
|
||||
-Lo 0 `
|
||||
-Hi ($mockManifest.Count - 1) `
|
||||
-GenerationCache $mockGenerationCache
|
||||
|
||||
$mockManifest[0].Title | Should -Be 'foobar0'
|
||||
|
||||
$mockManifest[1].Title | Should -Be 'foobar1'
|
||||
|
||||
$mockManifest[2].Title | Should -Be 'foobar2'
|
||||
|
||||
$mockManifest[3].Title | Should -Be 'foobar5'
|
||||
|
||||
$mockManifest[4].Title | Should -Be 'foobar3'
|
||||
|
||||
$mockManifest[5].Title | Should -Be 'foobar4'
|
||||
}
|
||||
}
|
||||
}
|
||||
361
tools/Manifest.psm1
Executable file
361
tools/Manifest.psm1
Executable file
|
|
@ -0,0 +1,361 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
|
||||
$script:schema = Get-Content (
|
||||
Join-Path $PSScriptRoot 'manifest.schema.json'
|
||||
) | Out-String
|
||||
|
||||
|
||||
function Get-Manifest
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Load the archive manifest
|
||||
|
||||
.EXAMPLE
|
||||
Get-Manifest 'manifest.json'
|
||||
#>
|
||||
Param(
|
||||
# filesystem location of manifest
|
||||
[Parameter(Mandatory)] [string] $File
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
try
|
||||
{
|
||||
$raw = Get-Content $File | Out-String
|
||||
}
|
||||
|
||||
catch
|
||||
{
|
||||
Write-Debug $_
|
||||
|
||||
$raw = '{"pages":{}, "attachments": {}}'
|
||||
}
|
||||
|
||||
$raw | Test-JSON -Schema $script:schema | Out-Null
|
||||
|
||||
$data = $raw | ConvertFrom-JSON
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Set-Manifest
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Dump the archive manifest
|
||||
|
||||
.EXAMPLE
|
||||
Set-Manifest 'manifest.json'
|
||||
#>
|
||||
Param(
|
||||
# manifest object
|
||||
[Parameter(Mandatory)] [PSObject] $Manifest,
|
||||
# filesystem location of manifest
|
||||
[Parameter(Mandatory)] [string] $File,
|
||||
# create a backup first
|
||||
[Parameter()] [bool] $Backup = $false
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
$raw = $Manifest | ConvertTo-JSON
|
||||
|
||||
$raw | Test-JSON -Schema $script:schema
|
||||
|
||||
if ($Backup)
|
||||
{
|
||||
$baseDir = Split-Path $File
|
||||
|
||||
$baseName = "$(Split-Path -Leaf $File).bck"
|
||||
|
||||
#FIXME: this should be handled without an explicit condition
|
||||
if ($baseDir)
|
||||
{
|
||||
$path = Join-Path $baseDir $baseName
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
$path = $baseName
|
||||
}
|
||||
|
||||
Copy-Item -Path $File -Destination $path
|
||||
}
|
||||
|
||||
Set-Content -Path $File -Value $raw
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function New-AncestralPageGenerationCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Calculate the numeric ancestral generation of a page
|
||||
|
||||
.DESCRIPTION
|
||||
The Get-AncestralPageGeneration calculates a numeric ancestral
|
||||
generation of a page, which is used for sorting.
|
||||
|
||||
The index required as input can be retrieved through the
|
||||
New-PagesManifestIndex function.
|
||||
|
||||
.EXAMPLES
|
||||
$generation = Get-AncestralPageGeneration `
|
||||
-Title 'foobar4' `
|
||||
-Manifest @() `
|
||||
-Index @{}
|
||||
#>
|
||||
Param(
|
||||
# Pages manifest
|
||||
[Parameter(Mandatory)] [Array] $Manifest,
|
||||
# Title of page to calculate generation of
|
||||
[Parameter()] [String] $Title,
|
||||
# Index for lookup of page metadata manifest item position
|
||||
[Parameter()] [Collections.Hashtable] $Index
|
||||
)
|
||||
|
||||
Begin
|
||||
{
|
||||
$cache = @{}
|
||||
|
||||
If (-Not $Index)
|
||||
{
|
||||
Write-Debug "rebuilding index"
|
||||
|
||||
$Index = ,$Manifest | New-PagesManifestIndex
|
||||
}
|
||||
}
|
||||
|
||||
Process
|
||||
{
|
||||
ForEach ($pageMeta in $Manifest)
|
||||
{
|
||||
$generation = 0
|
||||
|
||||
$pageMeta = $Title ? $Manifest[$Index.$Title] : $pageMeta
|
||||
|
||||
$ancestor = $pageMeta.AncestorTitle
|
||||
|
||||
$pageMeta_ = $pageMeta
|
||||
|
||||
While ($ancestor)
|
||||
{
|
||||
$generation += 1
|
||||
|
||||
$pageMeta_ = $Manifest[$Index."$($pageMeta_.AncestorTitle)"]
|
||||
|
||||
$ancestor = $pageMeta_.AncestorTitle
|
||||
}
|
||||
|
||||
$cache[$pageMeta.Title] = $generation
|
||||
|
||||
if ($Title) {Break}
|
||||
}
|
||||
}
|
||||
|
||||
End {$cache}
|
||||
}
|
||||
|
||||
|
||||
function Optimize-PagesManifest
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sort Pages Manifest in accordance with the pages ancestry
|
||||
|
||||
.DESCRIPTION
|
||||
The Optimize-PagesManifest function sorts a Pages manifest in
|
||||
accordance with the pages ancestry. This makes sure that an ancestor
|
||||
is already published, before its descendant is published.
|
||||
|
||||
The sorting is done with a quick-sort algorithm, using the Hoare
|
||||
partitioning scheme.
|
||||
|
||||
Older/lower ancestral generations take precedence over
|
||||
higher/younger ones. Syblings within a generation are treated
|
||||
as LIFO, where the youngest (last) has precedence over the oldest
|
||||
(fist).
|
||||
|
||||
.EXAMPLE
|
||||
$manifest Optimize-PagesManifest -Manifest ,@()
|
||||
|
||||
or
|
||||
|
||||
$manifest = ,@() | Optimize-PagesManifest
|
||||
|
||||
.NOTES
|
||||
whichever system generates the manifest should already output the
|
||||
array in the correct order, however it is not wise to depend upon
|
||||
this.
|
||||
#>
|
||||
Param(
|
||||
# Manifest to sort
|
||||
[Parameter(Mandatory)] [Array] $Manifest,
|
||||
# Left partition border
|
||||
[Parameter(Mandatory)] [Int] $Lo,
|
||||
# Right partition border
|
||||
[Parameter(Mandatory)] [Int] $Hi,
|
||||
# Cache for storing the numeric ancestral generation of pages
|
||||
[Parameter(Mandatory)] [Collections.Hashtable] $GenerationCache
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
$pivotPageMeta = $Manifest[($Lo + $Hi) / 2]
|
||||
|
||||
$pivot = $generationCache[$pivotPageMeta.Title]
|
||||
|
||||
# left index
|
||||
$i = $Lo
|
||||
|
||||
# right index
|
||||
$j = $Hi
|
||||
|
||||
While($i -le $j)
|
||||
{
|
||||
# Move the left index to the right at least once and while
|
||||
# the element at the left index is less than the pivot
|
||||
While (
|
||||
$generationCache."$($Manifest[$i].Title)" -lt $pivot `
|
||||
-And `
|
||||
$i -lt $Hi
|
||||
)
|
||||
{
|
||||
$i += 1
|
||||
}
|
||||
|
||||
# Move the right index to the left at least once and while
|
||||
# element at the right index is greater than the pivot
|
||||
While (
|
||||
$generationCache."$($Manifest[$j].Title)" -gt $pivot `
|
||||
-And `
|
||||
$j -gt $Lo
|
||||
)
|
||||
{
|
||||
$j -= 1
|
||||
}
|
||||
|
||||
If ($i -le $j)
|
||||
{
|
||||
$tmp = $Manifest[$i]
|
||||
|
||||
$Manifest[$i] = $Manifest[$j]
|
||||
|
||||
$Manifest[$j] = $tmp
|
||||
|
||||
$i += 1
|
||||
|
||||
$j -= 1
|
||||
}
|
||||
|
||||
If ($Lo -lt $j)
|
||||
{
|
||||
Optimize-PagesManifest `
|
||||
-Manifest $Manifest `
|
||||
-Lo $Lo`
|
||||
-Hi $j `
|
||||
-GenerationCache $GenerationCache
|
||||
}
|
||||
|
||||
If ($i -lt $Hi)
|
||||
{
|
||||
Optimize-PagesManifest `
|
||||
-Manifest $Manifest `
|
||||
-Lo $i `
|
||||
-Hi $Hi `
|
||||
-GenerationCache $GenerationCache
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function New-PagesManifestIndex
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Create an index of pages from a manifest
|
||||
|
||||
.DESCRIPTION
|
||||
The New-PageIndex function builds a page index from a manifest
|
||||
for faster lookup of page metadata. The title of a page is used for
|
||||
indexing, since a page title is unique within a Confluence space.
|
||||
|
||||
.INPUTS
|
||||
Manifest
|
||||
|
||||
.OUTPUTS
|
||||
Returns a Hashtable, where the key of each key-value pair is the
|
||||
page title and attachment name, and the value is the index of the
|
||||
array item within the Attachments porition of the manifest.
|
||||
|
||||
.EXAMPLE
|
||||
New-PageIndex -Manifest @{}
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory, ValueFromPipeline)] [Array] $Manifest
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
$index = @{}
|
||||
|
||||
For($i = 0; $i -lt $Manifest.Count; $i += 1)
|
||||
{
|
||||
$index[$Manifest[$i].Title] = $i
|
||||
}
|
||||
|
||||
$index
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function New-AttachmentsManifestIndex
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Create an index of page container attachments from a manifest
|
||||
|
||||
.DESCRIPTION
|
||||
The New-AttachmentIndex function builds an attachment index from a
|
||||
manifest for faster lookup of attachment metadata. The title of
|
||||
the container page, including the attachment name a is used for
|
||||
indexing, since attachment names are unique within a container page.
|
||||
|
||||
.INPUTS
|
||||
Manifest
|
||||
|
||||
.OUTPUTS
|
||||
Returns a Hashtable, where the key of each key-value pair is the
|
||||
interpolation of the container page title and attachment name, and
|
||||
the value is the index of the array item within the Attachments
|
||||
porition of the manifest.
|
||||
|
||||
.EXAMPLE
|
||||
New-AttachmentIndex -Manifest @{}
|
||||
#>
|
||||
Param(
|
||||
#manifest
|
||||
[Parameter(Mandatory, ValueFromPipeline)] [Array] $Manifest
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
$index = @{}
|
||||
|
||||
For($i = 0; $i -lt $Manifest.Count; $i += 1)
|
||||
{
|
||||
$key = "$($Manifest[$i].ContainerPageTitle):" + `
|
||||
"$($Manifest[$i].Name)"
|
||||
|
||||
$index[$key] = $i
|
||||
}
|
||||
|
||||
$index
|
||||
}
|
||||
}
|
||||
|
||||
134
tools/PSConfluencePublisher.psd1
Executable file
134
tools/PSConfluencePublisher.psd1
Executable file
|
|
@ -0,0 +1,134 @@
|
|||
@{
|
||||
|
||||
# Script module or binary module file associated with this manifest.
|
||||
#RootModule = 'ConfluencePublisher.psm1'
|
||||
|
||||
ModuleVersion = '1.1.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
# ID used to uniquely identify this module
|
||||
GUID = 'b51d47f9-19b9-4c34-9a88-36eb8cf4c9bd'
|
||||
|
||||
# Author of this module
|
||||
Author = 'Theodor Rodweil'
|
||||
|
||||
# Company or vendor of this module
|
||||
CompanyName = 'Victory Karma IT'
|
||||
|
||||
# Copyright statement for this module
|
||||
Copyright = '(c) victory-k.it. All rights reserved.'
|
||||
|
||||
RootModule = 'PSConfluencePublisher.psm1'
|
||||
|
||||
# Description of the functionality provided by this module
|
||||
# Description = ''
|
||||
|
||||
# Minimum version of the PowerShell engine required by this module
|
||||
# PowerShellVersion = '6.0'
|
||||
|
||||
# Name of the PowerShell host required by this module
|
||||
# PowerShellHostName = ''
|
||||
|
||||
# Minimum version of the PowerShell host required by this module
|
||||
# PowerShellHostVersion = ''
|
||||
|
||||
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
|
||||
# DotNetFrameworkVersion = ''
|
||||
|
||||
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
|
||||
# ClrVersion = ''
|
||||
|
||||
# Processor architecture (None, X86, Amd64) required by this module
|
||||
# ProcessorArchitecture = ''
|
||||
|
||||
# Modules that must be imported into the global environment prior to importing this module
|
||||
# RequiredModules = @('sdf')
|
||||
|
||||
# Assemblies that must be loaded prior to importing this module
|
||||
# RequiredAssemblies = @()
|
||||
|
||||
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
|
||||
# ScriptsToProcess = @()
|
||||
|
||||
# Type files (.ps1xml) to be loaded when importing this module
|
||||
# TypesToProcess = @()
|
||||
|
||||
# Format files (.ps1xml) to be loaded when importing this module
|
||||
# FormatsToProcess = @()
|
||||
|
||||
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
|
||||
NestedModules = @(
|
||||
'PersonalAccessToken.psm1',
|
||||
'Connection.psm1',
|
||||
'Manifest.psm1',
|
||||
'Page.psm1',
|
||||
'PageMeta.psm1',
|
||||
'String.psm1'
|
||||
)
|
||||
|
||||
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
|
||||
FunctionsToExport = '*'
|
||||
|
||||
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
|
||||
CmdletsToExport = '*'
|
||||
|
||||
# Variables to export from this module
|
||||
VariablesToExport = '*'
|
||||
|
||||
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
|
||||
AliasesToExport = @()
|
||||
|
||||
# DSC resources to export from this module
|
||||
# DscResourcesToExport = @()
|
||||
|
||||
# List of all modules packaged with this module
|
||||
# ModuleList = @()
|
||||
|
||||
# List of all files packaged with this module
|
||||
FileList = @(
|
||||
"manifest.schema.json"
|
||||
)
|
||||
|
||||
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
|
||||
PrivateData = @{
|
||||
|
||||
PSData = @{
|
||||
|
||||
# Tags applied to this module. These help with module discovery in online galleries.
|
||||
# Tags = @()
|
||||
|
||||
# A URL to the license for this module.
|
||||
# LicenseUri = ''
|
||||
|
||||
# A URL to the main website for this project.
|
||||
# ProjectUri = ''
|
||||
|
||||
# A URL to an icon representing this module.
|
||||
# IconUri = ''
|
||||
|
||||
# ReleaseNotes of this module
|
||||
# ReleaseNotes = ''
|
||||
|
||||
# Prerelease string of this module
|
||||
# Prerelease = ''
|
||||
|
||||
# Flag to indicate whether the module requires explicit user acceptance for install/update/save
|
||||
# RequireLicenseAcceptance = $false
|
||||
|
||||
# External dependent modules of this module
|
||||
# ExternalModuleDependencies = @()
|
||||
|
||||
} # End of PSData hashtable
|
||||
|
||||
} # End of PrivateData hashtable
|
||||
|
||||
# HelpInfo URI of this module
|
||||
# HelpInfoURI = ''
|
||||
|
||||
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
|
||||
# DefaultCommandPrefix = ''
|
||||
|
||||
}
|
||||
|
||||
35
tools/PSConfluencePublisher.psm1
Executable file
35
tools/PSConfluencePublisher.psm1
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
#!/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"
|
||||
|
||||
|
||||
259
tools/Page.Tests.ps1
Executable file
259
tools/Page.Tests.ps1
Executable file
|
|
@ -0,0 +1,259 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
|
||||
BeforeAll {
|
||||
Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1')
|
||||
}
|
||||
|
||||
|
||||
Describe 'New-Page' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
BeforeAll `
|
||||
{
|
||||
Mock -ModuleName 'Page' Get-Content {
|
||||
'foobar content'
|
||||
}
|
||||
|
||||
Mock -ModuleName 'Page' Get-PersonalAccessToken {
|
||||
'01234567890123456789'
|
||||
}
|
||||
}
|
||||
|
||||
It 'succeeds' `
|
||||
{
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
'Ref' = 'pages/320okffs.xml'
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
Mock -ModuleName 'Page' Get-PageMeta {
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
Mock -ModuleName 'Page' Update-PageMeta {
|
||||
$Id | Should -Be '123'
|
||||
|
||||
$mockPageMeta.Id = '123'
|
||||
|
||||
$mockPageMeta.Version = 1
|
||||
|
||||
$mockPageMeta.Hash = 'NOTAREALHASH'
|
||||
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
Mock -ModuleName 'Page' Invoke-WebRequest {
|
||||
$Uri | Should -Be 'https://confluence.contoso.com/rest/api/content'
|
||||
|
||||
$body_ = $Body | ConvertFrom-JSON
|
||||
|
||||
$body_.type | Should -Be 'page'
|
||||
|
||||
$body_.body.storage.representation | Should -Be 'storage'
|
||||
|
||||
$body_.body.storage.value | Should -Be 'foobar content'
|
||||
|
||||
$body_.space.key | Should -Be 'testitest'
|
||||
|
||||
$body_.title | Should -Be 'title'
|
||||
|
||||
@{
|
||||
'Content' = '{"Id": "123", "version": {"number": 1}}'
|
||||
}
|
||||
}
|
||||
|
||||
New-Page `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Space 'testitest' `
|
||||
-Title 'title' `
|
||||
-Manifest $mockManifest
|
||||
|
||||
$mockPageMeta.Id | Should -Be "123"
|
||||
|
||||
$mockPageMeta.Version | Should -Be 1
|
||||
|
||||
$mockPageMeta.Hash | Should -Be (
|
||||
'NOTAREALHASH'
|
||||
)
|
||||
|
||||
Should -Invoke -CommandName 'Get-PageMeta' `
|
||||
-ModuleName 'Page' `
|
||||
-Exactly `
|
||||
-Times 1
|
||||
|
||||
Should -Invoke -CommandName 'Update-PageMeta' `
|
||||
-ModuleName 'Page' `
|
||||
-Exactly `
|
||||
-Times 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'Update-Page' `
|
||||
{
|
||||
BeforeAll `
|
||||
{
|
||||
Mock -ModuleName 'Page' Get-Content {
|
||||
'foobar content'
|
||||
}
|
||||
|
||||
Mock -ModuleName 'Page' Get-PersonalAccessToken {
|
||||
'01234567890123456789'
|
||||
}
|
||||
}
|
||||
|
||||
Context 'default' `
|
||||
{
|
||||
BeforeAll `
|
||||
{
|
||||
Mock -ModuleName 'Page' Get-StringHash {
|
||||
@{
|
||||
'Hash' = 'NOTAREALHASH'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
It 'succeeds' `
|
||||
{
|
||||
$mockPageId = '0123456789'
|
||||
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
'Ref' = 'pages/320okffs.xml'
|
||||
'Id' = $mockPageId
|
||||
'Version' = 3
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
Mock -ModuleName 'Page' Get-PageMeta {
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
Mock -ModuleName 'Page' Invoke-WebRequest {
|
||||
$Uri | Should -Be (
|
||||
'https://confluence.contoso.com/rest/api/content/' + `
|
||||
$mockPageId
|
||||
)
|
||||
|
||||
$body_ = $Body | ConvertFrom-JSON
|
||||
|
||||
$body_.type | Should -Be 'page'
|
||||
|
||||
$body_.body.storage.representation | Should -Be 'storage'
|
||||
|
||||
$body_.body.storage.value | Should -Be 'foobar content'
|
||||
|
||||
$body_.space.key | Should -Be 'testitest'
|
||||
|
||||
$body_.title | Should -Be 'foobar'
|
||||
|
||||
$body_.version.number | Should -Be 4
|
||||
|
||||
@{
|
||||
'Content' = '{"Id": "123", "version": {"number": 4}}'
|
||||
}
|
||||
}
|
||||
|
||||
Update-Page `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Space 'testitest' `
|
||||
-Title 'foobar' `
|
||||
-Manifest $mockManifest
|
||||
|
||||
$mockPageMeta.Hash | Should -Be 'NOTAREALHASH'
|
||||
|
||||
$mockPageMeta.Version | Should -Be 4
|
||||
}
|
||||
|
||||
It 'skips, if hash unchanged' `
|
||||
{
|
||||
$mockPageId = '0123456789'
|
||||
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
'Ref' = 'pages/320okffs.xml'
|
||||
'Id' = $mockPageId
|
||||
'Version' = 3
|
||||
'Hash' = 'NOTAREALHASH'
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
Mock -ModuleName 'Page' Get-PageMeta {
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
Update-Page `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Space 'testitest' `
|
||||
-Title 'mockTitle' `
|
||||
-Manifest $mockManifest
|
||||
}
|
||||
|
||||
It 'fails, if page meta has no reference' `
|
||||
{
|
||||
$mockPageId = '0123456789'
|
||||
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
'Id' = $mockPageId
|
||||
'Version' = 3
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
Mock -ModuleName 'Page' Get-PageMeta {
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
{
|
||||
Update-Page `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Space 'testitest' `
|
||||
-Title 'mockTitle' `
|
||||
-Manifest $mockManifest
|
||||
} | Should -Throw "no reference to local content for page*"
|
||||
}
|
||||
|
||||
It 'fails, if page meta has no id' `
|
||||
{
|
||||
$mockPageId = '0123456789'
|
||||
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
'Ref' = 'foo/bar'
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
Mock -ModuleName 'Page' Get-PageMeta {
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
{
|
||||
Update-Page `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Space 'testitest' `
|
||||
-Title 'mockTitle' `
|
||||
-Manifest $mockManifest
|
||||
} | Should -Throw "no id for page*"
|
||||
}
|
||||
}
|
||||
}
|
||||
279
tools/Page.psm1
Executable file
279
tools/Page.psm1
Executable file
|
|
@ -0,0 +1,279 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
|
||||
function New-Page
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Add a confluence page
|
||||
|
||||
.DESCRIPTION
|
||||
|
||||
.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(Mandatory)] [string] $Title,
|
||||
# pages manifest
|
||||
[Parameter(Mandatory)] [Array] $Manifest,
|
||||
# pages manifest index
|
||||
[Parameter()] [Collections.Hashtable] $Index
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
$pageMeta = Get-PageMeta `
|
||||
-Host $Host `
|
||||
-Space $Space `
|
||||
-Title $Title `
|
||||
-Manifest $Manifest `
|
||||
-Index $Index
|
||||
|
||||
if (-Not $pageMeta.Ref)
|
||||
{
|
||||
throw "no reference to local content for page `$Title`."
|
||||
}
|
||||
|
||||
$content = Get-Content -Path $pageMeta.Ref
|
||||
|
||||
$transportBody = @{
|
||||
'type' = 'page'
|
||||
'title' = $Title
|
||||
'space' = @{
|
||||
'key' = $Space
|
||||
}
|
||||
'body' = @{
|
||||
'storage' = @{
|
||||
'value' = $content
|
||||
'representation' = 'storage'
|
||||
}
|
||||
}
|
||||
} | ConvertTo-JSON
|
||||
|
||||
Invoke-WebRequest `
|
||||
-Uri "https://${Host}/rest/api/content" `
|
||||
-Method 'Post' `
|
||||
-Headers @{
|
||||
'Authorization' = "Bearer $(Get-PersonalAccessToken $Host)"
|
||||
} `
|
||||
-ContentType "application/json" `
|
||||
-Body $transportBody `
|
||||
-OutVariable rawResponse | Out-Null
|
||||
}
|
||||
|
||||
End
|
||||
{
|
||||
$response = ($rawResponse.Content | ConvertFrom-JSON)
|
||||
|
||||
Update-PageMeta `
|
||||
-Title $Title `
|
||||
-Id $response.Id `
|
||||
-Version $response.version.number `
|
||||
-Hash (Get-StringHash $content).Hash `
|
||||
-Manifest $Manifest `
|
||||
-Index $Index
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Update-Page
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Add a confluence page
|
||||
|
||||
.DESCRIPTION
|
||||
|
||||
.EXAMPLE
|
||||
Update-ConfluencePage
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Space 'TIARA' `
|
||||
-Title 'Testitest' `
|
||||
-Manifest @{}
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory)] [string] $Host,
|
||||
# The name of the Confluence space to publish to
|
||||
[Parameter(Mandatory)] [string] $Space,
|
||||
# title of page to be published
|
||||
[Parameter(Mandatory)] [string] $Title,
|
||||
# pages manifest
|
||||
[Parameter(Mandatory)] [Array] $Manifest,
|
||||
# pages manifest index
|
||||
[Parameter()] [Collections.Hashtable] $Index
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
$pageMeta = Get-PageMeta `
|
||||
-Host $Host `
|
||||
-Space $Space `
|
||||
-Title $Title `
|
||||
-Manifest $Manifest `
|
||||
-Index $Index
|
||||
|
||||
if (-Not $pageMeta.Ref)
|
||||
{
|
||||
throw "no reference to local content for page '$Title'."
|
||||
}
|
||||
|
||||
if (-Not $pageMeta.Id)
|
||||
{
|
||||
throw "no id for page '$Title'."
|
||||
}
|
||||
|
||||
$content = Get-Content -Path $pageMeta.Ref
|
||||
|
||||
$hash = (Get-StringHash $content).Hash
|
||||
|
||||
if ($hash -eq $pageMeta.Hash)
|
||||
{
|
||||
Write-Host "content unchanged, skipping: '$Title'"
|
||||
|
||||
# yep, this is funny... This behaves like a return statement, because
|
||||
# a cmdlet, treats the input as an array of inputs. We keep it that
|
||||
# way so that all functions can properly act upon pipes. See
|
||||
# additional information on 'Process' blocks.
|
||||
continue
|
||||
}
|
||||
|
||||
# we're not updating this in place, so that we don't have to reset the
|
||||
# value opon failure
|
||||
$version = $pageMeta.Version + 1
|
||||
|
||||
$transportBody = @{
|
||||
'id' = $PageMeta.Id
|
||||
'type' = 'page'
|
||||
'title' = $Title
|
||||
'space' = @{
|
||||
'key' = $Space
|
||||
}
|
||||
'body' = @{
|
||||
'storage' = @{
|
||||
'value' = $content
|
||||
'representation' = 'storage'
|
||||
}
|
||||
}
|
||||
'version' = @{
|
||||
'number' = $version
|
||||
}
|
||||
} | ConvertTo-JSON
|
||||
|
||||
Invoke-WebRequest `
|
||||
-Uri "https://${Host}/rest/api/content/$($PageMeta.Id)" `
|
||||
-Method 'Put' `
|
||||
-Headers @{
|
||||
'Authorization' = "Bearer $(Get-PersonalAccessToken $Host)"
|
||||
} `
|
||||
-ContentType "application/json" `
|
||||
-Body $transportBody `
|
||||
-OutVariable rawResponse | Out-Null
|
||||
}
|
||||
|
||||
End
|
||||
{
|
||||
$response = ($rawResponse.Content | ConvertFrom-JSON)
|
||||
|
||||
Update-PageMeta `
|
||||
-Title $Title `
|
||||
-Id $pageMeta.Id `
|
||||
-Version $response.version.number `
|
||||
-Hash $hash `
|
||||
-Manifest $Manifest `
|
||||
-Index $Index
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Publish-Page
|
||||
{
|
||||
Param(
|
||||
# title of the page (used for manifest lookup)
|
||||
[Parameter(Mandatory)] [string] $Title,
|
||||
# hostname of Confluence instance
|
||||
[Parameter(Mandatory)] [string] $Host,
|
||||
# name of Confluence space
|
||||
[Parameter(Mandatory)] [string] $Space,
|
||||
# manifest object
|
||||
[Parameter(Mandatory, ValueFromPipeline)] [PSObject] $Meta
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
ForEach($meta in $Meta)
|
||||
{
|
||||
$meta = Get-PageMeta `
|
||||
-Host $hostname `
|
||||
-Space $spaceName `
|
||||
-Title $Title `
|
||||
-Manifest $Manifest
|
||||
|
||||
if ($meta.AncestorTitle)
|
||||
{
|
||||
$ancestorPageMeta = Get-PageMeta `
|
||||
-Host $hostname `
|
||||
-Space $spaceName `
|
||||
-Title $pageMeta.AncestorTitle `
|
||||
-Manifest $Manifest
|
||||
|
||||
if (-Not ($ancestorPageMeta -Or $ancestorPageMeta.PageId))
|
||||
{
|
||||
Write-Host "ancestor, not published, skipping: $Title"
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (-Not $pageId)
|
||||
{
|
||||
Write-Host ("create ${_}: $prettyName")
|
||||
|
||||
try {
|
||||
New-Page `
|
||||
-Host $hostname `
|
||||
-Space $spaceName `
|
||||
-Title $Title `
|
||||
-Manifest $Manifest
|
||||
}
|
||||
|
||||
catch
|
||||
{
|
||||
Write-Host "error for '$Title', skipping: $_"
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
Write-Host ("update ${_} (${pageId}): $prettyName")
|
||||
|
||||
try
|
||||
{
|
||||
Update-Page `
|
||||
-Host $hostname `
|
||||
-Space $Space `
|
||||
-Title $Title `
|
||||
-Manifest $Manifest
|
||||
}
|
||||
|
||||
catch
|
||||
{
|
||||
Write-Host "error for '$Title', skipping: $_"
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
256
tools/PageMeta.Tests.ps1
Executable file
256
tools/PageMeta.Tests.ps1
Executable file
|
|
@ -0,0 +1,256 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
BeforeAll {
|
||||
Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1')
|
||||
}
|
||||
|
||||
|
||||
Describe 'Get-PageMetaCache' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
It 'uses index' `
|
||||
{
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
$mockIndex = @{
|
||||
'foobar' = 0
|
||||
}
|
||||
|
||||
$meta = Get-PageMetaCache `
|
||||
-Title 'foobar' `
|
||||
-Manifest $mockManifest `
|
||||
-Index $mockIndex
|
||||
|
||||
$meta | Should -Be $mockPageMeta
|
||||
}
|
||||
|
||||
It 'returns page meta when title exists' `
|
||||
{
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
$meta = Get-PageMetaCache `
|
||||
-Title 'foobar' `
|
||||
-Manifest $mockManifest
|
||||
|
||||
$meta | Should -Be $mockPageMeta
|
||||
}
|
||||
|
||||
It 'returns null, if page with supplied title does not exist' `
|
||||
{
|
||||
$mockManifest = @(
|
||||
@{}
|
||||
)
|
||||
|
||||
$meta = Get-PageMetaCache `
|
||||
-Title 'foobar' `
|
||||
-Manifest $mockManifest
|
||||
|
||||
$meta | Should -Be $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'Get-PageMeta' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
BeforeAll `
|
||||
{
|
||||
Mock -ModuleName 'PageMeta' Get-PersonalAccessToken {
|
||||
'012345678901234567890'
|
||||
}
|
||||
}
|
||||
|
||||
It 'returns cache when page id present' `
|
||||
{
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
'Id' = '0123456789'
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
Mock -ModuleName 'PageMeta' Get-PageMetaCache {
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
$meta = Get-PageMeta `
|
||||
-Host 'foobar' `
|
||||
-Title 'foobar' `
|
||||
-Space 'foobar' `
|
||||
-Manifest $mockManifest
|
||||
|
||||
$meta | Should -Be $mockPageMeta
|
||||
|
||||
Should -Invoke -CommandName 'Get-PageMetaCache' `
|
||||
-ModuleName 'PageMeta' `
|
||||
-Exact `
|
||||
-Times 1
|
||||
}
|
||||
|
||||
It 'gets a page id remotely if there is exactly one result' `
|
||||
{
|
||||
$mockPageMeta = @{
|
||||
'Version' = 'version'
|
||||
'Hash' = 'hash'
|
||||
'Ref' = 'ref'
|
||||
}
|
||||
|
||||
Mock -ModuleName 'PageMeta' Get-PageMetaCache {
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
Mock -ModuleName 'PageMeta' Update-PageMeta {
|
||||
$Id | Should -Be '123'
|
||||
|
||||
$Version | Should -Be 9
|
||||
|
||||
$Title | Should -Be 'foobar'
|
||||
|
||||
$mockPageMeta
|
||||
}
|
||||
|
||||
Mock -ModuleName 'PageMeta' Invoke-WebRequest {
|
||||
@{
|
||||
'Content' = '{"results": [{"id": "123","_expandable":{"version": 9}}]}'
|
||||
}
|
||||
}
|
||||
|
||||
$meta = Get-PageMeta `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Title 'foobar' `
|
||||
-Space 'foobar' `
|
||||
-Manifest @{'Pages'= {}}
|
||||
|
||||
$meta | Should -Be $mockPageMeta
|
||||
|
||||
Should -Invoke 'Get-PageMetaCache' `
|
||||
-ModuleName 'PageMeta' `
|
||||
-Exactly `
|
||||
-Times 1
|
||||
|
||||
Should -Invoke 'Invoke-WebRequest' `
|
||||
-ModuleName 'PageMeta' `
|
||||
-Exactly `
|
||||
-Times 1
|
||||
|
||||
Should -Invoke 'Update-PageMeta' `
|
||||
-ModuleName 'PageMeta' `
|
||||
-Exactly `
|
||||
-Times 1
|
||||
}
|
||||
|
||||
It 'throws an exception, if there is more than one result' `
|
||||
{
|
||||
Mock -ModuleName 'PageMeta' Invoke-WebRequest {
|
||||
@{
|
||||
'Content' = '{"results": [{}, {}]}'
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Get-PageMeta `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Title 'foobar' `
|
||||
-Space 'foobar' `
|
||||
-Manifest @{'Pages'= {}}
|
||||
} | Should -Throw 'more than one result for query*'
|
||||
}
|
||||
|
||||
It 'throws an exception, if there is no result' `
|
||||
{
|
||||
Mock -ModuleName 'PageMeta' Invoke-WebRequest {
|
||||
@{
|
||||
'Content' = '{"results": []}'
|
||||
}
|
||||
}
|
||||
|
||||
$result = Get-PageMeta `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Title 'foobar' `
|
||||
-Space 'foobar' `
|
||||
-Manifest @{'Pages'= {}}
|
||||
|
||||
$result | Should -Be $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'Update-PageMeta' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
It 'fails, if page meta index does not exist' `
|
||||
{
|
||||
{
|
||||
Update-PageMeta `
|
||||
-Id '0123456789' `
|
||||
-Title 'foobar' `
|
||||
-Manifest @{}
|
||||
} | Should -Throw
|
||||
}
|
||||
|
||||
It 'updates minimal' `
|
||||
{
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
$pageMeta = Update-PageMeta `
|
||||
-Title 'foobar' `
|
||||
-Id '0123456789' `
|
||||
-Manifest $mockManifest
|
||||
|
||||
$mockPageMeta.Id | Should -Be '0123456789'
|
||||
}
|
||||
|
||||
It 'updates extended' `
|
||||
{
|
||||
$mockPageMeta = @{
|
||||
'Title' = 'foobar'
|
||||
}
|
||||
|
||||
$mockManifest = @(
|
||||
$mockPageMeta
|
||||
)
|
||||
|
||||
Update-PageMeta `
|
||||
-Title 'foobar' `
|
||||
-Id 'pageId' `
|
||||
-Version 9001 `
|
||||
-AncestorTitle 'ancestorTitle' `
|
||||
-Hash 'hash' `
|
||||
-Manifest $mockManifest
|
||||
|
||||
$mockPageMeta.Id | Should -Be 'pageId'
|
||||
|
||||
$mockPageMeta.Version | Should -Be 9001
|
||||
|
||||
$mockPageMeta.AncestorTitle | Should -Be 'ancestorTitle'
|
||||
|
||||
$mockPageMeta.Hash | Should -Be 'hash'
|
||||
}
|
||||
}
|
||||
}
|
||||
200
tools/PageMeta.psm1
Executable file
200
tools/PageMeta.psm1
Executable file
|
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
|
||||
function Get-PageMetaCache
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get a locally indexed/cached Confluence page id
|
||||
|
||||
.EXAMPLE
|
||||
Get-PageMetaCache `
|
||||
-Title 'Page Title' `
|
||||
-Manifest @() `
|
||||
-Index @{}
|
||||
|
||||
.NOTES
|
||||
To test or not to test, that is the question... Since the
|
||||
`Test-JSON` cmdlet requires serialized JSON, but we are working with
|
||||
the deserialized Hashtable, it's too computationally intense to
|
||||
always test the input upon every call. We therefore only make sure,
|
||||
that correct data is written to the filesystem. For the rest, each
|
||||
function is responsible for themself (learned that that's a valid
|
||||
reflexive pronoun today 🤓).
|
||||
|
||||
This function is lucky to get this note, because it's at the top 💯.
|
||||
Of course this applies to every function.
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory)] [string] $Title,
|
||||
[Parameter(Mandatory)] [Array] $Manifest,
|
||||
[Parameter()] [Collections.Hashtable] $Index
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
If ($Index -And $Manifest.Count -gt 0 -And $Manifest[$Index.$Title])
|
||||
{
|
||||
$Manifest[$Index.$Title]
|
||||
}
|
||||
|
||||
Else
|
||||
{
|
||||
For ($i = 0; $i -lt $Manifest.Count; $i += 1)
|
||||
{
|
||||
If ($Manifest[$i].Title -eq $Title)
|
||||
{
|
||||
$Manifest[$i]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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(
|
||||
[Parameter(Mandatory)] [string] $Host,
|
||||
[Parameter(Mandatory)] [string] $Title,
|
||||
[Parameter(Mandatory)] [string] $Space,
|
||||
[Parameter(Mandatory)] [Array] $Manifest,
|
||||
[Parameter()] [Collections.Hashtable] $Index
|
||||
)
|
||||
|
||||
Begin
|
||||
{
|
||||
$pageMeta = Get-PageMetaCache `
|
||||
-Title $Title `
|
||||
-Manifest $Manifest `
|
||||
-Index $Index
|
||||
}
|
||||
|
||||
Process
|
||||
{
|
||||
If ($pageMeta -And $pageMeta.Id)
|
||||
{
|
||||
$pageMeta
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
$escapedTitle = [Uri]::EscapeDataString($Title)
|
||||
|
||||
#TODO: move this to a separate function
|
||||
$query = "title=${escapedTitle}&spaceKey=${Space}&expand=history"
|
||||
|
||||
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)
|
||||
{
|
||||
throw "more than one result for query: $query"
|
||||
}
|
||||
|
||||
elseif ($results.Count -eq 1)
|
||||
{
|
||||
Update-PageMeta `
|
||||
-Id $results[0].id `
|
||||
-Version ($results[0]._expandable | Select -ExpandProperty 'version') `
|
||||
-Title $Title `
|
||||
-Manifest $Manifest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Update-PageMeta
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Register a Confluence page's metadata in the local cache
|
||||
|
||||
.DESCRIPTION
|
||||
Synchronizes the locally cached page metadata (in manifest) with the
|
||||
data stored by the Confluence instance. Therefore it is required to
|
||||
supply a page id, since this is the reference linking the locally
|
||||
cached page to a published instance of a page.
|
||||
|
||||
.EXAMPLE
|
||||
Update-PageMeta `
|
||||
-Title 'foobar' `
|
||||
-PageId 'pageId' `
|
||||
-Version 9001 `
|
||||
-AncestorTitle 'ancestorTitle' `
|
||||
-Hash 'hash' `
|
||||
-Manifest $mockManifest
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory)] [String] $Title,
|
||||
# remote Confluence page instance id
|
||||
[Parameter(Mandatory)] [String] $Id,
|
||||
[Parameter()] [Int] $Version,
|
||||
[Parameter()] [String] $AncestorTitle,
|
||||
[Parameter()] [String] $Hash,
|
||||
[Parameter(Mandatory)] [Array] $Manifest,
|
||||
[Parameter()] [Collections.Hashtable] $Index
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
$pageMeta = Get-PageMetaCache `
|
||||
-Title $Title `
|
||||
-Manifest $Manifest `
|
||||
-Index $Index
|
||||
|
||||
If (-Not $pageMeta)
|
||||
{
|
||||
throw "page titled `$Title` not indexed in Manifest."
|
||||
}
|
||||
|
||||
$pageMeta.Id = $Id
|
||||
|
||||
If ($Version)
|
||||
{
|
||||
$pageMeta.Version = $Version
|
||||
}
|
||||
|
||||
If ($AncestorTitle)
|
||||
{
|
||||
$pageMeta.AncestorTitle = $AncestorTitle
|
||||
}
|
||||
|
||||
# if content didn't update, hash stays the same
|
||||
If ($Hash)
|
||||
{
|
||||
$pageMeta.Hash = $Hash
|
||||
}
|
||||
|
||||
Write-Debug "register: $Title -> $PageId"
|
||||
|
||||
$pageMeta
|
||||
}
|
||||
}
|
||||
|
||||
65
tools/PersonalAccessToken.Tests.ps1
Executable file
65
tools/PersonalAccessToken.Tests.ps1
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
BeforeAll {
|
||||
Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1')
|
||||
|
||||
$mockHost = 'confluence.contoso.com'
|
||||
|
||||
$mockPat = '01234567890123456789'
|
||||
}
|
||||
|
||||
|
||||
Describe 'Register-PersonalAccessToken' `
|
||||
{
|
||||
BeforeEach {
|
||||
Initialize-PersonalAccessTokenStore
|
||||
}
|
||||
|
||||
Context 'Parameterized' {
|
||||
|
||||
It 'throws no exception' {
|
||||
Register-PersonalAccessToken -Host $mockHost -Token $mockPat
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Shorthand' {
|
||||
|
||||
It 'throws no exception' {
|
||||
Register-PersonalAccessToken $mockHost $mockPat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Describe 'Get-PersonalAccessToken' `
|
||||
{
|
||||
BeforeEach {
|
||||
Initialize-PersonalAccessTokenStore
|
||||
}
|
||||
|
||||
Context 'Parameterized' {
|
||||
|
||||
It 'gets an existing PAT' {
|
||||
|
||||
Register-PersonalAccessToken -Host $mockHost -Token $mockPat
|
||||
|
||||
Get-PersonalAccessToken -Host $mockHost | Should -Be $mockPat
|
||||
}
|
||||
|
||||
It 'requires PAT to exist' {
|
||||
|
||||
{Get-PersonalAccessToken -Host $mockHost} | Should -Throw
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Shorthand' {
|
||||
|
||||
It 'throws no exception' {
|
||||
|
||||
Register-PersonalAccessToken -Host $mockHost -Token $mockPat
|
||||
|
||||
Get-PersonalAccessToken $mockHost | Should -Be $mockPat
|
||||
}
|
||||
}
|
||||
}
|
||||
100
tools/PersonalAccessToken.psm1
Executable file
100
tools/PersonalAccessToken.psm1
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Utilities for working with Confluence Personal Access Tokens
|
||||
|
||||
.DESCRIPTION
|
||||
|
||||
|
||||
|
||||
.EXAMPLE
|
||||
|
||||
Register-PersonalAccessToken `
|
||||
-Host 'confluence.contoso.com' `
|
||||
-Token '123456789123456789'
|
||||
|
||||
Get-PersonalAccessToken -Host 'confluence.contoso.com'
|
||||
#>
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
|
||||
# session storage of Confluence personal access tokens, scoped to this nested
|
||||
# module
|
||||
$script:PATS = @{}
|
||||
|
||||
|
||||
function Initialize-PersonalAccessTokenStore
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Initialize the store within this script's scope.
|
||||
|
||||
.EXAMPLE
|
||||
Initialize-PersonalAccessTokenStore
|
||||
#>
|
||||
Process
|
||||
{
|
||||
$script:PATS = @{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Register-PersonalAccessToken
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Register a Confluence Personal Access Token (PAT)
|
||||
|
||||
.DESCRIPTION
|
||||
The PAT is stored in the pseudo-local ``script`` scope as a
|
||||
SecureString. Implementors of functions accessing PATs MUST stall
|
||||
conversion to plain text string until the string is actually needed
|
||||
|
||||
.EXAMPLE
|
||||
Register-PersonalAccessToken confluence.contoso.com 0123456789
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
|
||||
Param(
|
||||
[Parameter(Mandatory, Position = 0)] [string] $Host,
|
||||
[Parameter(Mandatory, Position = 1)] [string] $Token
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
if ($script:PATS[$Host])
|
||||
{
|
||||
Write-Debug "PAT for '$Host' already registered, overwriting."
|
||||
}
|
||||
|
||||
$script:PATS[$Host] = ConvertTo-SecureString $Token -AsPlainText -Force
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Get-PersonalAccessToken
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get a Confluence Personal Access Token (PAT) registered in this
|
||||
script scope.
|
||||
|
||||
.EXAMPLE
|
||||
Get-PersonalAccessToken confluence.contoso.com
|
||||
#>
|
||||
Param(
|
||||
# Confluence instance hostname
|
||||
[Parameter(Mandatory, Position = 0)] [string] $Host
|
||||
)
|
||||
|
||||
Process
|
||||
{
|
||||
if (-Not $PATS[$Host])
|
||||
{
|
||||
throw "No personal access token for host '$Host' registered. " +
|
||||
"Hint: Call ``Register-PersonalAccessToken``"
|
||||
}
|
||||
|
||||
$([Net.NetworkCredential]::new('', $script:PATS[$Host]).Password)
|
||||
}
|
||||
}
|
||||
21
tools/String.Tests.ps1
Executable file
21
tools/String.Tests.ps1
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
BeforeAll {
|
||||
Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1')
|
||||
}
|
||||
|
||||
Describe 'Get-StringHash' `
|
||||
{
|
||||
Context 'default' `
|
||||
{
|
||||
It 'works' `
|
||||
{
|
||||
$result = Get-StringHash 'foobar'
|
||||
|
||||
$result.Hash | Should -Be (
|
||||
'C3AB8FF13720E8AD9047DD3946' + `
|
||||
'6B3C8974E592C2FA383D4A3960714CAEF0C4F2')
|
||||
}
|
||||
}
|
||||
}
|
||||
42
tools/String.psm1
Executable file
42
tools/String.psm1
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env pwsh
|
||||
|
||||
function Get-StringHash
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get hash value of a string
|
||||
|
||||
.DESCRIPTION
|
||||
The Get-StringHash function is just a wrapper around the
|
||||
Get-FileHash function and utilizes a stream for providing said
|
||||
function with proper input values.
|
||||
|
||||
.OUTPUTS
|
||||
Same as the Get-FileHash function
|
||||
|
||||
.EXAMPLE
|
||||
Get-StringHash 'foobar' -Algorithm 'SHA256'
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory, Position = 0)] [String] $InputString,
|
||||
[Parameter()] [String] $Algorithm = 'SHA256'
|
||||
)
|
||||
|
||||
Begin
|
||||
{
|
||||
$stream = [IO.MemoryStream]::New()
|
||||
|
||||
$writer = [IO.StreamWriter]::New($stream)
|
||||
|
||||
$writer.Write($InputString)
|
||||
|
||||
$writer.Flush()
|
||||
|
||||
$stream.Position = 0
|
||||
}
|
||||
|
||||
Process
|
||||
{
|
||||
Get-FileHash -InputStream $stream -Algorithm $Algorithm
|
||||
}
|
||||
}
|
||||
98
tools/manifest.schema.json
Executable file
98
tools/manifest.schema.json
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"$id": "https://spec.victory-k.it/psconfluencepublisher.json",
|
||||
"x-authors": [
|
||||
"theodor.rodweil@victory-k.it"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Pages": {
|
||||
"type": "array",
|
||||
"item": {
|
||||
"$ref": "#/definitions/page"
|
||||
}
|
||||
},
|
||||
"Attachments": {
|
||||
"type": "array",
|
||||
"item": {
|
||||
"$ref": "#/definitions/attachment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Pages",
|
||||
"Attachments"
|
||||
],
|
||||
"definitions": {
|
||||
"page": {
|
||||
"type": "object",
|
||||
"description": "Local Confluence page/container attachment metadata",
|
||||
"properties": {
|
||||
"Title": {
|
||||
"type": "string",
|
||||
"description": "Title of page"
|
||||
},
|
||||
"Id": {
|
||||
"type": "string",
|
||||
"description": "Id of attachment defined by Confluence instance. The id is generated after the publishing of a page."
|
||||
},
|
||||
"Version": {
|
||||
"type": "string"
|
||||
},
|
||||
"Hash": {
|
||||
"type": "string",
|
||||
"description": "SHA512 hexadecimal content hash value"
|
||||
},
|
||||
"Ref": {
|
||||
"type": "string",
|
||||
"description": "Local filesystem reference/path"
|
||||
},
|
||||
"AncestorTitle": {
|
||||
"type": "string",
|
||||
"description": "Title of Confluence page this page is a child of. The title must be a property key of the pages object."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Title",
|
||||
"Ref"
|
||||
]
|
||||
},
|
||||
"attachment": {
|
||||
"type": "object",
|
||||
"description": "Local Confluence page/container attachment metadata",
|
||||
"properties": {
|
||||
"Name": {
|
||||
"type": "string",
|
||||
"description": "name of attachment, which must be unique within the container page"
|
||||
},
|
||||
"Id": {
|
||||
"type": "string",
|
||||
"description": "Id of attachment defined by Confluence instance. The id is generated after the publishing of an attachment."
|
||||
},
|
||||
"Hash": {
|
||||
"type": "string",
|
||||
"description": "SHA512 hexadecimal attachment content hash value"
|
||||
},
|
||||
"MimeType": {
|
||||
"type": "string",
|
||||
"description": "MIME type of attachment",
|
||||
"default": "binary/octet-stream"
|
||||
},
|
||||
"ContainerPageTitle": {
|
||||
"type": "string",
|
||||
"description": "Title of Confluence page this attachment is contained in. The title must be a property key of the pages object."
|
||||
},
|
||||
"Ref": {
|
||||
"type": "string",
|
||||
"description": "Local filesystem reference/path"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Name",
|
||||
"Hash",
|
||||
"MimeType",
|
||||
"ContainerPageTitle",
|
||||
"Ref"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
19
tools/scripts.deps.json
Executable file
19
tools/scripts.deps.json
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETStandard,Version=v2.0/",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETStandard,Version=v2.0": {},
|
||||
".NETStandard,Version=v2.0/": {
|
||||
"PSConfluencePublisher/1.0.0": {
|
||||
"dependencies": {
|
||||
"NETStandard.Library": "2.0.3"
|
||||
},
|
||||
"runtime": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue