From 17266ecb99ec281d48ed8ea80aee6b1adbd29b30 Mon Sep 17 00:00:00 2001 From: "Rodweil, Theodor" Date: Sun, 30 Jul 2023 16:16:55 +0200 Subject: [PATCH] init --- .gitignore | 3 + .nuspec | 14 + PSConfluencePublisher/._.DS_Store | Bin 0 -> 4096 bytes PSConfluencePublisher/._Connection.Tests.ps1 | Bin 0 -> 4096 bytes PSConfluencePublisher/._Connection.psm1 | Bin 0 -> 4096 bytes PSConfluencePublisher/._Manifest.Tests.ps1 | Bin 0 -> 4096 bytes PSConfluencePublisher/._Manifest.psm1 | Bin 0 -> 4096 bytes .../._PSConfluencePublisher.psd1 | Bin 0 -> 4096 bytes .../._PSConfluencePublisher.psm1 | Bin 0 -> 4096 bytes PSConfluencePublisher/._Page.psm1 | Bin 0 -> 4096 bytes .../._PersonalAccessToken.Tests.ps1 | Bin 0 -> 4096 bytes .../._PersonalAccessToken.psm1 | Bin 0 -> 4096 bytes PSConfluencePublisher/._manifest.schema.json | Bin 0 -> 4096 bytes PSConfluencePublisher/._scripts.deps.json | Bin 0 -> 4096 bytes PSConfluencePublisher/Connection.Tests.ps1 | 65 +++ PSConfluencePublisher/Connection.psm1 | 54 ++ PSConfluencePublisher/Manifest.Tests.ps1 | 70 +++ PSConfluencePublisher/Manifest.psm1 | 74 +++ .../PSConfluencePublisher.psd1 | 131 +++++ .../PSConfluencePublisher.psm1 | 508 ++++++++++++++++++ PSConfluencePublisher/Page.psm1 | 435 +++++++++++++++ .../PersonalAccessToken.Tests.ps1 | 65 +++ .../PersonalAccessToken.psm1 | 99 ++++ .../_mock/._test-manifest1.json | Bin 0 -> 4096 bytes .../_mock/._test-manifest2.json | Bin 0 -> 4096 bytes .../_mock/test-manifest1.json | 4 + .../_mock/test-manifest2.json | 12 + PSConfluencePublisher/manifest.schema.json | 93 ++++ PSConfluencePublisher/scripts.deps.json | 19 + README.md | 85 +++ 30 files changed, 1731 insertions(+) create mode 100755 .gitignore create mode 100755 .nuspec create mode 100755 PSConfluencePublisher/._.DS_Store create mode 100755 PSConfluencePublisher/._Connection.Tests.ps1 create mode 100755 PSConfluencePublisher/._Connection.psm1 create mode 100755 PSConfluencePublisher/._Manifest.Tests.ps1 create mode 100755 PSConfluencePublisher/._Manifest.psm1 create mode 100755 PSConfluencePublisher/._PSConfluencePublisher.psd1 create mode 100755 PSConfluencePublisher/._PSConfluencePublisher.psm1 create mode 100755 PSConfluencePublisher/._Page.psm1 create mode 100755 PSConfluencePublisher/._PersonalAccessToken.Tests.ps1 create mode 100755 PSConfluencePublisher/._PersonalAccessToken.psm1 create mode 100755 PSConfluencePublisher/._manifest.schema.json create mode 100755 PSConfluencePublisher/._scripts.deps.json create mode 100755 PSConfluencePublisher/Connection.Tests.ps1 create mode 100755 PSConfluencePublisher/Connection.psm1 create mode 100755 PSConfluencePublisher/Manifest.Tests.ps1 create mode 100755 PSConfluencePublisher/Manifest.psm1 create mode 100755 PSConfluencePublisher/PSConfluencePublisher.psd1 create mode 100755 PSConfluencePublisher/PSConfluencePublisher.psm1 create mode 100755 PSConfluencePublisher/Page.psm1 create mode 100755 PSConfluencePublisher/PersonalAccessToken.Tests.ps1 create mode 100755 PSConfluencePublisher/PersonalAccessToken.psm1 create mode 100755 PSConfluencePublisher/_mock/._test-manifest1.json create mode 100755 PSConfluencePublisher/_mock/._test-manifest2.json create mode 100755 PSConfluencePublisher/_mock/test-manifest1.json create mode 100755 PSConfluencePublisher/_mock/test-manifest2.json create mode 100755 PSConfluencePublisher/manifest.schema.json create mode 100755 PSConfluencePublisher/scripts.deps.json create mode 100755 README.md diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..657afab --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/_* +/._* +.DS_Store diff --git a/.nuspec b/.nuspec new file mode 100755 index 0000000..6e7a6a3 --- /dev/null +++ b/.nuspec @@ -0,0 +1,14 @@ + + + + ConfluencePublisher + 1.1.0 + Übergangslösung für die Integration von sphinx-confluencebuilder generierter Confluence Dokumentation + tiara.rodney@adesso.de + irina.ternovykh@vkb.de + UNLICENSED + README.md + VKBit Betrieb GmbH + Powershell Confluence Publisher + + diff --git a/PSConfluencePublisher/._.DS_Store b/PSConfluencePublisher/._.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..9ad849cdb9e467065f3aa98e6e57ed2552be42dd GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIhYCu0iY;W;207T zWIgNTe~1o-3apAo1xG_*Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLx9R5zz8%C zguy^ABqOs}p(wRDzqBYhRUs|EC|e;juOv0EBr`uRF(;=|AtyDhL?J0BF)tg~)rIOB Q(iE!y;a-tpko*510KSwTa{vGU literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._Connection.Tests.ps1 b/PSConfluencePublisher/._Connection.Tests.ps1 new file mode 100755 index 0000000000000000000000000000000000000000..41649b9f229f479ebde86db005449039fa4f4753 GIT binary patch literal 4096 zcmeH~u};G<5QZ;^s$!{_5TFuSVrW@Hp@;#*z{pU6+6=5ta&4nHb`_@xUWG?s;vG8i z8oWq)Rs#}QSV6jz?6dFw-;%%WptaYAEr5}X#r;G)iaJ~kc4(agblxe`z4Hm+`E>pz zZo_bV{rXYWj(w6JH7wiolB@3m9L6ROc&8J52Do`!B~Cw^ zuZRFZU-X7OadJ7lIvWgJfrAM!0Vco%m;e)C0!)AjFaajO1pXWW8ur}4w+TO-r&bhb r&9aD5OihuARF|mTm?)Kc5elcJh?LYB&B9-{|D1pR|NAFh@T+PK?j=rJ literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._Connection.psm1 b/PSConfluencePublisher/._Connection.psm1 new file mode 100755 index 0000000000000000000000000000000000000000..c8dd7789a44004675a1201fec906d1b8f226db45 GIT binary patch literal 4096 zcmeH~y-ve05XUcws$!{__)-;FVrW?c6uQ8~$WT$W8Cad<3Q-)pic&@Kz`I46s_R5~p8} z_cvjE(Yxr0li}d%{A}nlurUE9zyz286JP>NfC(@GCcp%kz@H=F(U08Nm>)i-RupK> qqKHwpz zZo_bV{rXYWj(w6JH7wiolB@3m9L6ROc&8J52Do`!B~Cw^ zuZRFZU-X7OadJ7lIvWgJfrAM!0Vco%m;e)C0!)AjFaajO1pXWW8ur}4w+TO-r&bhb r&9aD5OihuARF|mTm?)Kc5elcJh?LYB&B9-{|D1pR|NAFh@T+PK?j=rJ literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._Manifest.psm1 b/PSConfluencePublisher/._Manifest.psm1 new file mode 100755 index 0000000000000000000000000000000000000000..35de1407e4debc9ddd2fe37fa227eff0df967239 GIT binary patch literal 4096 zcmeH~u}Z{15QZmma@ZW&h{_QbEbgv}B8Y{RrG=d2Qdrn*k~t08>>k-Y(O2;)1Ruf5 z*YHK+EQx|?Y!zl;cXoIFe_+3EU~_i}8vs`_6Z3_*5ncE<*rIg;FnXa(_t6JH5YqXV zxJZ)t{Q14<9D5|+?^y=a$-i#{9Au`7s#*igLr4= z^HfaECSrE#%^XaC2`~XBzyz286JP>NfC(@GCh#{2&|v6?!#2X?zObT3Yu0s!VrlA9 j6xyNoW1>{*Nhn-8kt(T6nuWh?|2hBs|MySE5H`&x=x|JU literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._PSConfluencePublisher.psd1 b/PSConfluencePublisher/._PSConfluencePublisher.psd1 new file mode 100755 index 0000000000000000000000000000000000000000..af4eebfd8eee0970ab8b76ced0068222fedb4616 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vvYvJF zKST$^7oc)cG%d_PK1f`Efk7%cKUXiYpdcqzFDJ3Mq_j9SB_**WRXHFSqKA3!I`kVFiwfg1HF_%bHP-LZM9>20Hng{?O(TV8I>9@D_otV{ z=^rMKZ5R8ZHyriE;cz?{oQy{<#l{4f025#WOn?b60Vco%m;e)C0)LEvNBubeG(Wsa uttil%Wf7y8n<5jbE>XL0qEzZ$D4drfQc`F17p|-Rb@(~|_b6TPtLg*wm`^GI literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._Page.psm1 b/PSConfluencePublisher/._Page.psm1 new file mode 100755 index 0000000000000000000000000000000000000000..59f0c23568765f6e22c17674d02eacc1ab1e66b5 GIT binary patch literal 4096 zcmeH~zfQw25XLWvs$!{_5TGiu#L%*o0HqQzF)|=f8zELFxk41juHqEYnYZXuAYOrq z*WewP!xw_xDj=@9IR100O&lC(!BEq;Q2KE zCCWWRWi$=*R?d0<70Nb(22XZ!3I8f4-bBjqLOWe7deARe;(l)@B^W`!w zOis&o5E+x@vdAngdwbNi0Q`o>qQbaLjUG#Vi}ij(QaZoe@ZP63jTD~i1RnsdpI;K^ ze|%q#oc2ZUs3&&!hsP(Q!ypJ;j)Ms>0Vco%m;e)C0!)AjFaajO1pXEQkFMj|XZYb= uYDIz8%!?Ss)D)RWb&1-26QxoMp>SG?NJ*X1U%0ON*YflI-%sg+UsWFw_D`h% literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._PersonalAccessToken.Tests.ps1 b/PSConfluencePublisher/._PersonalAccessToken.Tests.ps1 new file mode 100755 index 0000000000000000000000000000000000000000..bc21001a10c03992ac8a91467a830314e8ebbe63 GIT binary patch literal 4096 zcmeH~ze>bF5XL8>95#nGqH=@jqXfIm$a5m%xKe+H{G4gk7Olxg362M9u1 z{}Ly0JU)JSs~g8A$$Kr!GF9^DYXIA+DI+U$N6JP>NfC(@GCcp%k025#WOyKVkpk~iEe2WOD rt(jLTim9mzk!y$Aw~11zd!caZM53fFs2BdS{pbAi|KC69L0H$HeyC3W literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._PersonalAccessToken.psm1 b/PSConfluencePublisher/._PersonalAccessToken.psm1 new file mode 100755 index 0000000000000000000000000000000000000000..b3bbd6c92f3d6df482f94628496e5846cc1de183 GIT binary patch literal 4096 zcmeH~y-EZz5XWczKy3~yQ8|}@!kr+?ab6=@^tUK0R$nP zf3cG|9v|Po)=kDH$#+^QOH|3n*8#RuQ$}(&Qy3}fs=31~lNIh>Oull0KKWIEEw3wO zYE4PcE=r6kN@)v6&%uB;>M(3+94bt!+~~2?SJ>2}iC__++tS|TE>9Gm>I`oI9v)vZ zuYW#x-}AmNdZR&4><>prhrR$C6JP>NfC(@GCcp%k025#WOn?deJpuu>P{$}Rw7YS7xWiyvi`OEIsf-4T?p&?1E|1G@c;k- literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._manifest.schema.json b/PSConfluencePublisher/._manifest.schema.json new file mode 100755 index 0000000000000000000000000000000000000000..b755fd43f255e5a74f015f978f2c2d256d5880ab GIT binary patch literal 4096 zcmeH~u}T9$5QgW}AZdb?poC+={vawCw6YWtLvlqr+3Zbp$=+_b-H1Mhtxw@QSos=0 zfrb4!n;e2^tOGMJ%+BmTF!OZ-t^Hlt0=SV%+>XRpG!|>nCO-z~yiugQ^9hio^!~># z^L%*m`cbbs;G5(JYc1Q9$NCky==W~OZkv=Ru7KIFWG#IUVk{yaIfHdk#sVY~} zx6eEpR6#pCl=dDQZJe0&-8Jo__IhIQ$`<$p@bdLuME!MgN816M_0LcGz250Ei3K($ zzyz286JP>NfC(@GCcp%k02A0q0trnWeQ9a_puMQjyG5l?OkFh-+6J`ol$6Fi3xm@j XCPvyB-NH@RMu)%q|E|)7w61>u9XdrR literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/._scripts.deps.json b/PSConfluencePublisher/._scripts.deps.json new file mode 100755 index 0000000000000000000000000000000000000000..809588f92b138e6f93c3dd6e2940600b24fa8eb2 GIT binary patch literal 4096 zcmeH~u}%Xq42FHs3b9n3I-u$liG}IZc59g!7#I+!y@55&xezs%RF@Q$N8lBB3f_T< z*Wel0ua2%D(S^B^Ek(AI|Ci!#Gqigra0GBGmAIRU2eFHI2d8vK0G)Tr)OWrBl9cLy z;wsOlmv5hSgEzLo4Z!v5M-lb)=3*H0a4;E-2eIN{0!)Aj zFaajO1egF5U;<2l2{3^}CO`{eTnd{=^C#^^h2E_zg<|2VrO-B@jjN1Ti<# UE@>D3u^n>$JOA$_eMswi3&{>dy#N3J literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/Connection.Tests.ps1 b/PSConfluencePublisher/Connection.Tests.ps1 new file mode 100755 index 0000000..3ad6c80 --- /dev/null +++ b/PSConfluencePublisher/Connection.Tests.ps1 @@ -0,0 +1,65 @@ +#!/usr/bin/env pwsh +$ErrorActionPreference = "Stop" + +BeforeAll { + Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1') -Force +} + + +Describe 'Test-Connection' ` +{ + + Context 'Parameterized' { + + It 'throws no exception' { + + InModuleScope Connection { + + Mock Get-PersonalAccessToken {'01234567890123456789'} + + Mock Invoke-WebRequest { + return @{ + 'Content' = "{'type': 'known'}" + 'StatusCode' = 200 + } + } + + Test-Connection -Host 'confluence.contoso.com' + } + } + + It 'detects anonymous authentication' { + + InModuleScope Connection { + + Mock Get-PersonalAccessToken {'01234567890123456789'} + + Mock Invoke-WebRequest { + return @{ + 'Content' = "{'type': 'anonymous'}" + 'StatusCode' = 200 + } + } + + {Test-Connection -Host 'confluence.contoso.com'} | Should -Throw + } + } + + It 'detects non 200 status codes' { + + InModuleScope Connection { + + Mock Get-PersonalAccessToken {'01234567890123456789'} + + Mock Invoke-WebRequest { + return @{ + 'Content' = "{'type': 'anonymous'}" + 'StatusCode' = 500 + } + } + + {Test-Connection -Host 'confluence.contoso.com'} | Should -Throw + } + } + } +} diff --git a/PSConfluencePublisher/Connection.psm1 b/PSConfluencePublisher/Connection.psm1 new file mode 100755 index 0000000..efb0b01 --- /dev/null +++ b/PSConfluencePublisher/Connection.psm1 @@ -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))" + } + } +} diff --git a/PSConfluencePublisher/Manifest.Tests.ps1 b/PSConfluencePublisher/Manifest.Tests.ps1 new file mode 100755 index 0000000..2674a9f --- /dev/null +++ b/PSConfluencePublisher/Manifest.Tests.ps1 @@ -0,0 +1,70 @@ +#!/usr/bin/env pwsh +$ErrorActionPreference = "Stop" + +BeforeAll { + Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1') -Force + +} + +AfterAll { + +} + + +Describe 'Get-Manifest' ` +{ + + Context 'Parameterized' { + + It 'throws no exception' { + + InModuleScope Connection { + + Mock Get-PersonalAccessToken {'01234567890123456789'} + + Mock Invoke-WebRequest { + return @{ + 'Content' = "{'type': 'known'}" + 'StatusCode' = 200 + } + } + + Test-Connection -Host 'confluence.contoso.com' + } + } + + It 'detects anonymous authentication' { + + InModuleScope Connection { + + Mock Get-PersonalAccessToken {'01234567890123456789'} + + Mock Invoke-WebRequest { + return @{ + 'Content' = "{'type': 'anonymous'}" + 'StatusCode' = 200 + } + } + + {Test-Connection -Host 'confluence.contoso.com'} | Should -Throw + } + } + + It 'detects non 200 status codes' { + + InModuleScope Connection { + + Mock Get-PersonalAccessToken {'01234567890123456789'} + + Mock Invoke-WebRequest { + return @{ + 'Content' = "{'type': 'anonymous'}" + 'StatusCode' = 500 + } + } + + {Test-Connection -Host 'confluence.contoso.com'} | Should -Throw + } + } + } +} diff --git a/PSConfluencePublisher/Manifest.psm1 b/PSConfluencePublisher/Manifest.psm1 new file mode 100755 index 0000000..dd52e27 --- /dev/null +++ b/PSConfluencePublisher/Manifest.psm1 @@ -0,0 +1,74 @@ +#!/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 + } + + catch + { + $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) + { + Copy-Item -Path $File -Destination "$(Split-Path -Leaf $File).bck" + } + + Set-Content -Path $File -Value $raw + } +} diff --git a/PSConfluencePublisher/PSConfluencePublisher.psd1 b/PSConfluencePublisher/PSConfluencePublisher.psd1 new file mode 100755 index 0000000..35859d5 --- /dev/null +++ b/PSConfluencePublisher/PSConfluencePublisher.psd1 @@ -0,0 +1,131 @@ +@{ + +# 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' +) + +# 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 = '' + +} + diff --git a/PSConfluencePublisher/PSConfluencePublisher.psm1 b/PSConfluencePublisher/PSConfluencePublisher.psm1 new file mode 100755 index 0000000..f8c5e53 --- /dev/null +++ b/PSConfluencePublisher/PSConfluencePublisher.psm1 @@ -0,0 +1,508 @@ +#!/usr/bin/env pwsh +<# + .SYNOPSIS + PowerShell Publisher for sphinxcontrib.confluencebuilder + + .DESCRIPTION + + - support for ancestral pages and containered attachments + - creates new pages if they don't exist + - updates existing pages and attachments if checksum mismatches + + .EXAMPLE + + Import-Module (Join-Path 'vendor' 'tiara.rodney' + 'PSConfluencePublisher' + 'PSConfluencePublisher' + 'PSConfluencePublisher.psd1') + + Register-PersonalAccessToken ` + -Host 'confluence.contoso.com' ` + -Token '123456789123456789' + + Test-Connection confluence.contoso.com + + Publish-All ` + -Url 'https://confluence.contoso.com/display/TIARA/Testitest' ` + -DumpIndex build/docs/confluence.out/data.json + + .NOTES + - tested with PowerShell Core (PSVersion 7.3.6) + - tested with PowerShell Desktop (PSVersion 5.1.19041.3031) +#> +$ErrorActionPreference = "Stop" + + +function New-ConfluencePage +{ + <# + .SYNOPSIS + Add a confluence page + + .DESCRIPTION + + .EXAMPLE + Add-ConfluencePage + -Host 'confluence.contoso.com' ` + -Space 'TIARA' ` + -Title 'Testitest' ` + -Content @{} + #> + 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, + # content of page + [Parameter(Mandatory)] [string] $Content, + # parent page id + [Parameter()] [string] $Ancestor + ) + + Process + { + Assert-PersonalAccessToken $Host + + $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 $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)" + } ` + -ContentType "application/json" ` + -Body $transportBody ` + -OutVariable rawResponse | Out-Null + } + + End + { + $response = ($rawResponse.Content | ConvertFrom-JSON) + + @{ + 'PageId' = $response.Id + 'Version' = $response.version | Select -ExpandProperty 'number' + } + } +} + + +function Update-Page +{ + <# + .SYNOPSIS + Add a confluence page + + .DESCRIPTION + + .EXAMPLE + Add-ConfluencePage + -Host 'confluence.contoso.com' ` + -Space 'TIARA' ` + -Title 'Testitest' ` + -Content @{} + #> + Param( + [Parameter(Mandatory)] [string] $Host, + # The page id of an existing page + [Parameter(Mandatory)] [string] $PageId, + # The name of the Confluence space to publish to + [Parameter(Mandatory)] [string] $Space, + # title of page to be published + [Parameter(Mandatory)] [string] $Title, + # version of content + [Parameter(Mandatory)] [int] $Version, + # content of page + [Parameter(Mandatory)] [string] $Content, + # parent page id + [Parameter()] [string] $Ancestor + ) + + Process + { + Assert-PersonalAccessToken $Host + + $transportBody = @{ + 'id' = $PageId + '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/$PageId" ` + -Method 'Put' ` + -Headers @{ + 'Authorization' = "Bearer $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)" + } ` + -ContentType "application/json" ` + -Body $transportBody ` + -OutVariable rawResponse | Out-Null + } + + End + { + $response = ($rawResponse.Content | ConvertFrom-JSON) + } +} + + +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)] [string] $CacheIndexFile + ) + + Process + { + if ($Title) + { + $cachedPageMeta = Get-CachedPageMeta ` + -Title $Title ` + -CacheIndexFile $CacheIndexFile + } + + if ($cachedPageMeta) + { + return $cachedPageMeta + } + + $escapedTitle = [uri]::EscapeDataString($Title) + + $query = "title=${escapedTitle}&spaceKey=${Space}&expand=history" + + Assert-PersonalAccessToken $Host + + Invoke-WebRequest ` + -Uri "https://${Host}/rest/api/content?$query" ` + -Method 'Get' ` + -Headers @{ + 'Authorization' = "Bearer $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)" + } ` + -OutVariable response + + $results = ($response.Content | ConvertFrom-JSON).results + + if ($results.Count -gt 1) + { + throw "more than one result for query: $query" + } + elseif ($results.Count -eq 1) + { + Register-PageMeta ` + -PageId $results[0].id ` + -Version ($results[0]._expandable | Select -ExpandProperty 'version') ` + -Title $Title ` + -CacheIndexFile $CacheIndexFile + } + } +} + + +function Get-CachedPageMeta +{ + <# + .SYNOPSIS + Get a locally indexed/cached Confluence page id + + .EXAMPLE + Get-CachedPageMeta ` + -Title 'd231cc3422bfdf96.xml' ` + -CacheIndexFile 'confluence-page-cache.json' + #> + Param( + [Parameter(Mandatory)] [string] $Title, + [Parameter(Mandatory)] [string] $CacheIndexFile + ) + + Process + { + try + { + $raw = Get-Content $CacheIndexFile + } + + catch + { + $raw = "{}" + } + + $data = $raw | ConvertFrom-JSON + + try + { + $pageMeta = $data | Select -ExpandProperty $Title + + $pageMeta + + Write-Debug "page id cache hit: $Title -> $($pageMeta.PageId)" + } + + catch + { + $null + + Write-Debug "page id cache miss: $Title" + } + } +} + + +function Register-PageMeta +{ + <# + .SYNOPSIS + Register a Confluence page's metadata in the local cache + + .DESCRIPTION + + .EXAMPLE + Add-ConfluencePage + -Host 'confluence.contoso.com' ` + -Space 'TIARA' ` + -Title 'Testitest' ` + -Content @{} + #> + Param( + [Parameter(Mandatory)] [string] $PageId, + [Parameter()] [int] $Version = 0, + [Parameter(Mandatory)] [string] $Title, + [Parameter()] [string] $ContentHash = '', + [Parameter(Mandatory)] [string] $CacheIndexFile + ) + + Process + { + try + { + $raw = Get-Content $CacheIndexFile + } + + catch + { + $raw = "{}" + } + + $data = $raw | ConvertFrom-JSON + + $data | Add-Member -Name $Title ` + -Value @{ + 'PageId' = $PageId + 'Version' = $Version + 'ContentHash' = $ContentHash + } ` + -MemberType NoteProperty ` + -Force + + Set-Content -Path $CacheIndexFile -Value ($data | ConvertTo-JSON) + + Write-Debug "indexed page id: $Title -> $PageId" + } +} + + +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)] [PSObject] $Manifest + ) + + Begin + { + $pageMeta = Get-PageMeta ` + -Host $hostname ` + -Space $spaceName ` + -Title $Title ` + -Manifest $Manifest + } + + Process + { + if ($pageMeta.ContentHash -eq $_) + { + Write-Host "skipping (no changes): $Title" + + return + } + + $pageId = $pageMeta.PageId + + $path = Join-Path $basepath 'content' "$_" + + $pageContent = Get-Content $path | Out-String + + $prettyName = $Title + + if ($data.pages[$_].ancestor_id) + { + $ancestorTitle = $data.pages[$data.pages[$_].ancestor_id].title + + $ancestorPageMeta = Get-PageMeta ` + -Host $hostname ` + -Space $spaceName ` + -Title $ancestorTitle ` + -CacheIndexFile $cacheIndexFile + + if ($ancestorPageMeta) + { + $ancestorPageId = $ancestorPageMeta.PageId + } + + $prettyName += " [$ancestorPageId]" + } + + if (-Not $pageId) + { + Write-Host ("create ${_}: $prettyName") + + try { + $pageMeta = New-ConfluencePage ` + -Host $hostname ` + -Space $spaceName ` + -Title $pageTitle ` + -Content $pageContent ` + -Ancestor $ancestorPageId + } + + catch + { + Write-Host "error (skipping): $prettyName" + + return + } + + + Register-PageMeta ` + -PageId $pageMeta.PageId ` + -Version $pageMeta.Version ` + -Title $pageTitle ` + -ContentHash $_ ` + -CacheIndexFile $cacheIndexFile + } + else + { + Write-Host ("update ${_} (${pageId}): $prettyName") + + $version = $pageMeta.Version + 1 + + try + { + Update-Page ` + -Host $hostname ` + -PageId $pageId ` + -Space $spaceName ` + -Title $pageTitle ` + -Version $version ` + -Content $pageContent ` + -Ancestor $ancestorPageId + } + + catch + { + Write-Host "error (skipping): $prettyName" + + return + } + + Register-PageMeta ` + -PageId $pageMeta.PageId ` + -Version $version ` + -Title $pageTitle ` + -ContentHash $_ ` + -CacheIndexFile $cacheIndexFile + } + } +} + + +function Publish-All +{ + <# + .SYNOPSIS + 1. cast index hash table to array + 2. (quick) sort the array + + .EXAMPLE + Get-Help -Name Test-Help + + This shows the help for the example function. + #> + Param( + [Parameter(Mandatory, Position = 0)] [string] $Url, + [Parameter(Mandatory, Position = 1)] [string] $Manifest + ) + + Begin + { + $hostname = ([uri]$url).Host + + $spaceName = (Split-Path -Leaf (Split-Path $Url)) + + $ancestorName = Split-Path -Leaf $url + + $data = Get-Content -Raw $Manifest | ConvertFrom-JSON -AsHashtable + + $basepath = Split-Path $Manifest + + $cacheIndexFile = 'confluence-page-cache.json' + } + + Process + { + $data.pages.keys | ForEach-Object ` + { + + } + } +} diff --git a/PSConfluencePublisher/Page.psm1 b/PSConfluencePublisher/Page.psm1 new file mode 100755 index 0000000..66da66a --- /dev/null +++ b/PSConfluencePublisher/Page.psm1 @@ -0,0 +1,435 @@ +#!/usr/bin/env pwsh +$ErrorActionPreference = "Stop" + + +function Get-CachedPageMeta +{ + <# + .SYNOPSIS + Get a locally indexed/cached Confluence page id + + .EXAMPLE + Get-CachedPageMeta ` + -Title 'd231cc3422bfdf96.xml' ` + -CacheIndexFile 'confluence-page-cache.json' + #> + Param( + [Parameter(Mandatory)] [string] $Title, + [Parameter(Mandatory)] [string] $CacheIndexFile + ) + + Process + { + try + { + $raw = Get-Content $CacheIndexFile + } + + catch + { + $raw = "{}" + } + + $data = $raw | ConvertFrom-JSON + + try + { + $pageMeta = $data | Select -ExpandProperty $Title + + $pageMeta + + Write-Debug "page id cache hit: $Title -> $($pageMeta.PageId)" + } + + catch + { + $null + + Write-Debug "page id cache miss: $Title" + } + } +} + + +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)] [string] $CacheIndexFile + ) + + Process + { + if ($Title) + { + $cachedPageMeta = Get-CachedPageMeta ` + -Title $Title ` + -CacheIndexFile $CacheIndexFile + } + + if ($cachedPageMeta) + { + return $cachedPageMeta + } + + $escapedTitle = [uri]::EscapeDataString($Title) + + $query = "title=${escapedTitle}&spaceKey=${Space}&expand=history" + + Assert-PersonalAccessToken $Host + + Invoke-WebRequest ` + -Uri "https://${Host}/rest/api/content?$query" ` + -Method 'Get' ` + -Headers @{ + 'Authorization' = "Bearer $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)" + } ` + -OutVariable response + + $results = ($response.Content | ConvertFrom-JSON).results + + if ($results.Count -gt 1) + { + throw "more than one result for query: $query" + } + elseif ($results.Count -eq 1) + { + Register-PageMeta ` + -PageId $results[0].id ` + -Version ($results[0]._expandable | Select -ExpandProperty 'version') ` + -Title $Title ` + -CacheIndexFile $CacheIndexFile + } + } +} + + +function Register-PageMeta +{ + <# + .SYNOPSIS + Register a Confluence page's metadata in the local cache + + .DESCRIPTION + + .EXAMPLE + Add-ConfluencePage + -Host 'confluence.contoso.com' ` + -Space 'TIARA' ` + -Title 'Testitest' ` + -Content @{} + #> + Param( + [Parameter(Mandatory)] [string] $PageId, + [Parameter()] [int] $Version = 0, + [Parameter(Mandatory)] [string] $Title, + [Parameter()] [string] $ContentHash = '', + [Parameter(Mandatory)] [string] $CacheIndexFile + ) + + Process + { + try + { + $raw = Get-Content $CacheIndexFile + } + + catch + { + $raw = "{}" + } + + $data = $raw | ConvertFrom-JSON + + $data | Add-Member -Name $Title ` + -Value @{ + 'PageId' = $PageId + 'Version' = $Version + 'ContentHash' = $ContentHash + } ` + -MemberType NoteProperty ` + -Force + + Set-Content -Path $CacheIndexFile -Value ($data | ConvertTo-JSON) + + Write-Debug "indexed page id: $Title -> $PageId" + } +} + + +function New-Page +{ + <# + .SYNOPSIS + Add a confluence page + + .DESCRIPTION + + .EXAMPLE + Add-ConfluencePage + -Host 'confluence.contoso.com' ` + -Space 'TIARA' ` + -Title 'Testitest' ` + -Content @{} + #> + 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, + # content of page + [Parameter(Mandatory)] [string] $Content, + # parent page id + [Parameter()] [string] $Ancestor + ) + + Process + { + Assert-PersonalAccessToken $Host + + $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 $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)" + } ` + -ContentType "application/json" ` + -Body $transportBody ` + -OutVariable rawResponse | Out-Null + } + + End + { + $response = ($rawResponse.Content | ConvertFrom-JSON) + + @{ + 'PageId' = $response.Id + 'Version' = $response.version | Select -ExpandProperty 'number' + } + } +} + + +function Update-Page +{ + <# + .SYNOPSIS + Add a confluence page + + .DESCRIPTION + + .EXAMPLE + Add-ConfluencePage + -Host 'confluence.contoso.com' ` + -Space 'TIARA' ` + -Title 'Testitest' ` + -Content @{} + #> + Param( + [Parameter(Mandatory)] [string] $Host, + # The page id of an existing page + [Parameter(Mandatory)] [string] $PageId, + # The name of the Confluence space to publish to + [Parameter(Mandatory)] [string] $Space, + # title of page to be published + [Parameter(Mandatory)] [string] $Title, + # version of content + [Parameter(Mandatory)] [int] $Version, + # content of page + [Parameter(Mandatory)] [string] $Content, + # parent page id + [Parameter()] [string] $Ancestor + ) + + Process + { + Assert-PersonalAccessToken $Host + + $transportBody = @{ + 'id' = $PageId + '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/$PageId" ` + -Method 'Put' ` + -Headers @{ + 'Authorization' = "Bearer $([System.Net.NetworkCredential]::new('', $script:PATS[$Host_]).Password)" + } ` + -ContentType "application/json" ` + -Body $transportBody ` + -OutVariable rawResponse | Out-Null + } + + End + { + $response = ($rawResponse.Content | ConvertFrom-JSON) + } +} + + +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)] [PSObject] $Manifest + ) + + Begin + { + $pageMeta = Get-PageMeta ` + -Host $hostname ` + -Space $spaceName ` + -Title $Title ` + -Manifest $Manifest + } + + Process + { + if ($pageMeta.ContentHash -eq $_) + { + Write-Host "skipping (no changes): $Title" + + return + } + + $pageId = $pageMeta.PageId + + $path = Join-Path $basepath 'content' "$_" + + $pageContent = Get-Content $path | Out-String + + $prettyName = $Title + + if ($data.pages[$_].ancestor_id) + { + $ancestorTitle = $data.pages[$data.pages[$_].ancestor_id].title + + $ancestorPageMeta = Get-PageMeta ` + -Host $hostname ` + -Space $spaceName ` + -Title $ancestorTitle ` + -CacheIndexFile $cacheIndexFile + + if ($ancestorPageMeta) + { + $ancestorPageId = $ancestorPageMeta.PageId + } + + $prettyName += " [$ancestorPageId]" + } + + if (-Not $pageId) + { + Write-Host ("create ${_}: $prettyName") + + try { + $pageMeta = New-Page ` + -Host $hostname ` + -Space $spaceName ` + -Title $pageTitle ` + -Content $pageContent ` + -Ancestor $ancestorPageId + } + + catch + { + Write-Host "error (skipping): $prettyName" + + return + } + + + Register-PageMeta ` + -PageId $pageMeta.PageId ` + -Version $pageMeta.Version ` + -Title $pageTitle ` + -ContentHash $_ ` + -CacheIndexFile $cacheIndexFile + } + else + { + Write-Host ("update ${_} (${pageId}): $prettyName") + + $version = $pageMeta.Version + 1 + + try + { + Update-Page ` + -Host $hostname ` + -PageId $pageId ` + -Space $spaceName ` + -Title $pageTitle ` + -Version $version ` + -Content $pageContent ` + -Ancestor $ancestorPageId + } + + catch + { + Write-Host "error (skipping): $prettyName" + + return + } + + Register-PageMeta ` + -PageId $pageMeta.PageId ` + -Version $version ` + -Title $pageTitle ` + -ContentHash $_ ` + -CacheIndexFile $cacheIndexFile + } + } +} \ No newline at end of file diff --git a/PSConfluencePublisher/PersonalAccessToken.Tests.ps1 b/PSConfluencePublisher/PersonalAccessToken.Tests.ps1 new file mode 100755 index 0000000..ea62a03 --- /dev/null +++ b/PSConfluencePublisher/PersonalAccessToken.Tests.ps1 @@ -0,0 +1,65 @@ +#!/usr/bin/env pwsh +$ErrorActionPreference = "Stop" + +BeforeAll { + Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1') -Force + + $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 + } + } +} \ No newline at end of file diff --git a/PSConfluencePublisher/PersonalAccessToken.psm1 b/PSConfluencePublisher/PersonalAccessToken.psm1 new file mode 100755 index 0000000..1f3a45c --- /dev/null +++ b/PSConfluencePublisher/PersonalAccessToken.psm1 @@ -0,0 +1,99 @@ +#!/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 +$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) + } +} \ No newline at end of file diff --git a/PSConfluencePublisher/_mock/._test-manifest1.json b/PSConfluencePublisher/_mock/._test-manifest1.json new file mode 100755 index 0000000000000000000000000000000000000000..5897760863a8cb1c243133cdcbbe20d844a3f479 GIT binary patch literal 4096 zcmeH~u}T9$5QgWBqG^H%T1Y$=><^8Fpp~VCD9IJJo4tuH+1m}bd!nyl=~MU)R(Xx| zcJ}8i7X{N;2WDXSXLkQ7=IaKw4z^(f;6^HOI}-O|8SxFelmmd?J1O-n0|&MfUxbkq-6oeHY+RxiXWpnZe9RA8L<9Ap;)YjoO)Hlaw-zzEY~nmGtd1 zj|NrH&JLx0z@>MGGy&+Zc%QV_V}sYWz-NH-&yOPdk6!D&sAm_0)00?mFaajO1egF5 zU;<2l2`~XBzyz4US`(myFdl_%r1_)vqC)TLN}-szYAUo1XyYj9UCqayjv{Sl; Rf5z5X|L*_0NgvXt`384fL_h!l literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/_mock/._test-manifest2.json b/PSConfluencePublisher/_mock/._test-manifest2.json new file mode 100755 index 0000000000000000000000000000000000000000..d26f896a40c913c8ee65e71a40ca806ef7c4f849 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vvYvJF zKST$^7oc)cG%d_PK1f`Efk7%cKUXiYpdcqzFDJ3Mq_j9SB_**WRXHFSqKAms4JP6lT)dXlbTkdkd%{{mksO- T!?X=$8rA=BpU5!C{r?XDp&UfZ literal 0 HcmV?d00001 diff --git a/PSConfluencePublisher/_mock/test-manifest1.json b/PSConfluencePublisher/_mock/test-manifest1.json new file mode 100755 index 0000000..8c3a0ae --- /dev/null +++ b/PSConfluencePublisher/_mock/test-manifest1.json @@ -0,0 +1,4 @@ +{ + "pages": {}, + "attachments": {} +} \ No newline at end of file diff --git a/PSConfluencePublisher/_mock/test-manifest2.json b/PSConfluencePublisher/_mock/test-manifest2.json new file mode 100755 index 0000000..5d2aa91 --- /dev/null +++ b/PSConfluencePublisher/_mock/test-manifest2.json @@ -0,0 +1,12 @@ +{ + "pages": { + "": { + "Ref": "" + } + }, + "attachments": { + "": { + + } + } +} \ No newline at end of file diff --git a/PSConfluencePublisher/manifest.schema.json b/PSConfluencePublisher/manifest.schema.json new file mode 100755 index 0000000..62be76f --- /dev/null +++ b/PSConfluencePublisher/manifest.schema.json @@ -0,0 +1,93 @@ +{ + "$id": "https://spec.victory-k.it/psconfluencepublisher.json", + "x-authors": [ + "theodor.rodweil@victory-k.it" + ], + "type": "object", + "properties": { + "pages": { + "type": "object", + "patternProperties": { + ".*": { + "$ref": "#/definitions/page" + } + } + }, + "attachments": { + "type": "object", + "patternProperties": { + ".*": { + "$ref": "#/definitions/attachment" + } + } + } + }, + "required": [ + "pages", + "attachments" + ], + "definitions": { + "page": { + "type": "object", + "description": "Local Confluence page/container attachment metadata", + "properties": { + "PageId": { + "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": [ + "Hash", + "Ref" + ] + }, + "attachment": { + "type": "object", + "description": "Local Confluence page/container attachment metadata", + "properties": { + "AttachmentId": { + "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": [ + "Hash", + "MimeType", + "ContainerPageTitle", + "Ref" + ] + } + } +} \ No newline at end of file diff --git a/PSConfluencePublisher/scripts.deps.json b/PSConfluencePublisher/scripts.deps.json new file mode 100755 index 0000000..adc9bd7 --- /dev/null +++ b/PSConfluencePublisher/scripts.deps.json @@ -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": {} +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..f2d30b0 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# PSConfluencePublisher + +This program is a standalone publisher component for the +`sphinxcontrib.confluencebuilder` Sphinx extension. + +It consumes, a JSON-formatted manifest of a *Sphinx build* dump generated by +the ``sphinxcontrib.xconfluencebuilder`` and unidirectionally synchronizes +pages, page ancestry, and attachments. + +Publishing is supported via the Confluence Server REST API through +[Personal Access Token (PAT) authorization](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html). + +## Usage + +You can install the module via [nuget](https://www.nuget.org). + +``` +Install-Module victorykit.PSConfluencePublisher +``` + +Alternatively, you can import the module from source. In order to do that, +clone the +[Git repository](https://bitbucket.org/victorykit/psconfluencepublisher/src) +, change into the directory and import it. + +``` +PS> git clone git@bitbucket.org:victorykit/psconfluencepublisher.git +``` + +``` +PS> # universal import statement compatible with PowerShell Core & Desktop +PS> Import-Module (Join-Path 'PSConfluencePublisher' + 'PSConfluencePublisher.psd1') +``` + +Next, register your personal access token for your Confluence server instance. +The token is stored as a *SecureString* within the *Script* scope. + +``` +Register-PersonalAccessToken ` + -Host 'confluence.contoso.com' ` + -Token '123456789123456789' +``` + +Optionally, you may test the connectivity to your Confluence instance. The test +will try to retrieve your user profile, in order to determine whether the PAT +authenticates, since an invalid PAT simply results in anonymous authentication +for some REST API functions. + +``` +Test-Connection confluence.contoso.com +``` + +Now you may publish by supplying the URL of the root Confluence page +you want to publish to, in addition to the location of the local dump manifest. +Make sure to use the full URL, with the same hostname as the one you used to +register your personal access token. + +``` +Publish-Dump ` + -Url 'https://confluence.contoso.com/display/TIARA/Testitest' ` + -DumpIndex build/docs/confluence.out/data.json +``` + +The manifest may be writable, where it is then used to cache the publishing +status of each page and attachment. + +You may publish a single page, which however requires it's direct ancestor page +to exist. + +``` +Publish-Page +``` + +## Debugging + +To display debug messages, set +[$DebugPreference](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.3#debugpreference) +to `Continue`, or `Inquire` in your shell's *Global* scope. + +## Testing + +This program requires [Pester](https://pester.dev/) to execute it's test suite. + +``PS> Invoke-Pester PSConfluencePublisher/*.Tests.ps1 -Show 'All'`` \ No newline at end of file