refactor: adapt to nuspec requirement for gallery publishing

This commit is contained in:
Rodweil, Theodor 2023-08-07 03:45:19 +02:00
parent 2019f8c959
commit 69c2684ee7
No known key found for this signature in database
GPG key ID: F8BC1B0EB1F9CCF5
29 changed files with 84 additions and 20 deletions

82
tools/Connection.Tests.ps1 Executable file
View 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
View 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
View 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
View 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
View 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 = ''
}

View 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
View 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
View 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
View 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
View 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
}
}

View 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
View 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
View 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
View 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
View 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
View 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": {}
}