Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/publish/confluence/confluence.ts
12926 views
1
import { join } from "../../deno_ral/path.ts";
2
import { Confirm, Input, Secret } from "cliffy/prompt/mod.ts";
3
import { RenderFlags } from "../../command/render/types.ts";
4
import { pathWithForwardSlashes } from "../../core/path.ts";
5
6
import {
7
readAccessTokens,
8
writeAccessToken,
9
writeAccessTokens,
10
} from "../common/account.ts";
11
12
import {
13
AccountToken,
14
AccountTokenType,
15
InputMetadata,
16
PublishFiles,
17
PublishProvider,
18
} from "../provider-types.ts";
19
20
import { PublishOptions, PublishRecord } from "../types.ts";
21
import { ConfluenceClient } from "./api/index.ts";
22
import {
23
AttachmentSummary,
24
ConfluenceParent,
25
ConfluenceSpaceChange,
26
Content,
27
ContentAncestor,
28
ContentBody,
29
ContentBodyRepresentation,
30
ContentChange,
31
ContentChangeType,
32
ContentCreate,
33
ContentProperty,
34
ContentPropertyKey,
35
ContentStatusEnum,
36
ContentSummary,
37
ContentUpdate,
38
LogPrefix,
39
PAGE_TYPE,
40
PublishContentResult,
41
PublishRenderer,
42
PublishType,
43
PublishTypeEnum,
44
SiteFileMetadata,
45
SitePage,
46
SpaceChangeResult,
47
User,
48
WrappedResult,
49
} from "./api/types.ts";
50
import { withSpinner } from "../../core/console.ts";
51
import {
52
buildFileToMetaTable,
53
buildPublishRecordForContent,
54
buildSpaceChanges,
55
confluenceParentFromString,
56
convertForSecondPass,
57
doWithSpinner,
58
filterFilesForUpdate,
59
findAttachments,
60
flattenIndexes,
61
footnoteTransform,
62
getNextVersion,
63
getTitle,
64
isContentCreate,
65
isContentDelete,
66
isContentUpdate,
67
isNotFound,
68
isUnauthorized,
69
mergeSitePages,
70
tokenFilterOut,
71
transformAtlassianDomain,
72
updateImagePaths,
73
updateLinks,
74
validateEmail,
75
validateParentURL,
76
validateServer,
77
validateToken,
78
wrapBodyForConfluence,
79
writeTokenComparator,
80
} from "./confluence-helper.ts";
81
82
import {
83
verifyAccountToken,
84
verifyConfluenceParent,
85
verifyLocation,
86
verifyOrWarnManagePermissions,
87
} from "./confluence-verify.ts";
88
import {
89
DELETE_DISABLED,
90
ATTACHMENT_UPLOAD_DELAY_MS,
91
DELETE_SLEEP_MILLIS,
92
DESCENDANT_PAGE_SIZE,
93
EXIT_ON_ERROR,
94
MAX_PAGES_TO_LOAD,
95
} from "./constants.ts";
96
import { logError, trace } from "./confluence-logger.ts";
97
import { md5HashBytes } from "../../core/hash.ts";
98
import { sleep } from "../../core/async.ts";
99
import { info } from "../../deno_ral/log.ts";
100
101
export const CONFLUENCE_ID = "confluence";
102
103
const getAccountTokens = (): Promise<AccountToken[]> => {
104
const getConfluenceEnvironmentAccount = () => {
105
const server = Deno.env.get("CONFLUENCE_DOMAIN");
106
const name = Deno.env.get("CONFLUENCE_USER_EMAIL");
107
const token = Deno.env.get("CONFLUENCE_AUTH_TOKEN");
108
if (server && name && token) {
109
return {
110
type: AccountTokenType.Environment,
111
name,
112
server: transformAtlassianDomain(server),
113
token,
114
};
115
}
116
};
117
118
const readConfluenceAccessTokens = (): AccountToken[] => {
119
const result = readAccessTokens<AccountToken>(CONFLUENCE_ID) ?? [];
120
return result;
121
};
122
123
let accounts: AccountToken[] = [];
124
125
const envAccount = getConfluenceEnvironmentAccount();
126
if (envAccount) {
127
accounts = [...accounts, envAccount];
128
}
129
130
const tempStoredAccessTokens = readConfluenceAccessTokens();
131
accounts = [...accounts, ...tempStoredAccessTokens];
132
return Promise.resolve(accounts);
133
};
134
135
const removeToken = (token: AccountToken) => {
136
const existingTokens = readAccessTokens<AccountToken>(CONFLUENCE_ID) ?? [];
137
138
const toWrite: Array<AccountToken> = existingTokens.filter((accessToken) =>
139
tokenFilterOut(accessToken, token)
140
);
141
142
writeAccessTokens(CONFLUENCE_ID, toWrite);
143
};
144
145
const promptAndAuthorizeToken = async () => {
146
const server: string = await Input.prompt({
147
indent: "",
148
message: "Confluence Domain:",
149
hint: "e.g. https://mydomain.atlassian.net/",
150
validate: validateServer,
151
transform: transformAtlassianDomain,
152
});
153
154
await verifyLocation(server);
155
156
const name = await Input.prompt({
157
indent: "",
158
message: `Confluence Account Email:`,
159
validate: validateEmail,
160
});
161
162
const token = await Secret.prompt({
163
indent: "",
164
message: "Confluence API Token:",
165
hint: "Create an API token at https://id.atlassian.com/manage/api-tokens",
166
validate: validateToken,
167
});
168
169
const accountToken: AccountToken = {
170
type: AccountTokenType.Authorized,
171
name,
172
server,
173
token,
174
};
175
await withSpinner(
176
{ message: "Verifying account..." },
177
() => verifyAccountToken(accountToken),
178
);
179
writeAccessToken<AccountToken>(
180
CONFLUENCE_ID,
181
accountToken,
182
writeTokenComparator,
183
);
184
185
return Promise.resolve(accountToken);
186
};
187
188
const promptForParentURL = async () => {
189
return await Input.prompt({
190
indent: "",
191
message: `Space or Parent Page URL:`,
192
hint: "Browse in Confluence to the space or parent, then copy the URL",
193
validate: validateParentURL,
194
});
195
};
196
197
const resolveTarget = async (
198
accountToken: AccountToken,
199
target: PublishRecord,
200
): Promise<PublishRecord> => {
201
return Promise.resolve(target);
202
};
203
204
const loadDocument = (baseDirectory: string, rootFile: string): ContentBody => {
205
const documentValue = Deno.readTextFileSync(join(baseDirectory, rootFile));
206
207
const body: ContentBody = wrapBodyForConfluence(documentValue);
208
209
return body;
210
};
211
212
const renderDocument = async (
213
render: PublishRenderer,
214
): Promise<PublishFiles> => {
215
const flags: RenderFlags = {
216
to: "confluence-publish",
217
};
218
219
return await render(flags);
220
};
221
222
const renderSite = async (render: PublishRenderer): Promise<PublishFiles> => {
223
const flags: RenderFlags = {
224
to: "confluence-publish",
225
};
226
227
const renderResult: PublishFiles = await render(flags);
228
return renderResult;
229
};
230
231
async function publish(
232
account: AccountToken,
233
type: PublishType,
234
_input: string,
235
title: string,
236
_slug: string,
237
render: (flags?: RenderFlags) => Promise<PublishFiles>,
238
_options: PublishOptions,
239
publishRecord?: PublishRecord,
240
): Promise<[PublishRecord, URL | undefined]> {
241
trace("publish", {
242
account,
243
type,
244
_input,
245
title,
246
_slug,
247
_options,
248
publishRecord,
249
});
250
251
const client = new ConfluenceClient(account);
252
253
const user: User = await client.getUser();
254
255
let parentUrl: string = publishRecord?.url ?? (await promptForParentURL());
256
257
const parent: ConfluenceParent = confluenceParentFromString(parentUrl);
258
259
const server = account?.server ?? "";
260
261
await verifyConfluenceParent(parentUrl, parent);
262
263
const space = await client.getSpace(parent.space);
264
265
trace("publish", { parent, server, id: space.id, key: space.key });
266
267
await verifyOrWarnManagePermissions(client, space, parent, user);
268
269
const uniquifyTitle = async (title: string, idToIgnore: string = "") => {
270
trace("uniquifyTitle", title);
271
272
const titleIsUnique: boolean = await client.isTitleUniqueInSpace(
273
title,
274
space,
275
idToIgnore,
276
);
277
278
if (titleIsUnique) {
279
return title;
280
}
281
282
const uuid = globalThis.crypto.randomUUID();
283
const shortUUID = uuid.split("-")[0] ?? uuid;
284
const uuidTitle = `${title} ${shortUUID}`;
285
286
return uuidTitle;
287
};
288
289
const fetchExistingSite = async (parentId: string): Promise<SitePage[]> => {
290
let descendants: ContentSummary[] = [];
291
let start = 0;
292
293
for (let i = 0; i < MAX_PAGES_TO_LOAD; i++) {
294
const result: WrappedResult<ContentSummary> = await client
295
.getDescendantsPage(parentId, start);
296
if (result.results.length === 0) {
297
break;
298
}
299
300
descendants = [...descendants, ...result.results];
301
302
start = start + DESCENDANT_PAGE_SIZE;
303
}
304
305
trace("descendants.length", descendants);
306
307
const contentProperties: ContentProperty[][] = await Promise.all(
308
descendants.map((page: ContentSummary) =>
309
client.getContentProperty(page.id ?? "")
310
),
311
);
312
313
const sitePageList: SitePage[] = mergeSitePages(
314
descendants,
315
contentProperties,
316
);
317
318
return sitePageList;
319
};
320
321
const uploadAttachments = (
322
baseDirectory: string,
323
attachmentsToUpload: string[],
324
parentId: string,
325
filePath: string,
326
existingAttachments: AttachmentSummary[] = [],
327
): Promise<AttachmentSummary | null>[] => {
328
const uploadAttachment = async (
329
attachmentPath: string,
330
): Promise<AttachmentSummary | null> => {
331
let fileBuffer: Uint8Array;
332
let fileHash: string;
333
const path = join(baseDirectory, attachmentPath);
334
335
trace(
336
"uploadAttachment",
337
{
338
baseDirectory,
339
attachmentPath,
340
attachmentsToUpload,
341
parentId,
342
existingAttachments,
343
path,
344
},
345
LogPrefix.ATTACHMENT,
346
);
347
348
try {
349
fileBuffer = await Deno.readFile(path);
350
fileHash = await md5HashBytes(fileBuffer);
351
} catch (error) {
352
logError(`${path} not found`, error);
353
return null;
354
}
355
356
const fileName = pathWithForwardSlashes(attachmentPath);
357
358
const existingDuplicateAttachment = existingAttachments.find(
359
(attachment: AttachmentSummary) => {
360
return attachment?.metadata?.comment === fileHash;
361
},
362
);
363
364
if (existingDuplicateAttachment) {
365
trace(
366
"existing duplicate attachment found",
367
existingDuplicateAttachment.title,
368
LogPrefix.ATTACHMENT,
369
);
370
return existingDuplicateAttachment;
371
}
372
373
const file = new File([fileBuffer as BlobPart], fileName);
374
const attachment: AttachmentSummary = await client
375
.createOrUpdateAttachment(parentId, file, fileHash);
376
377
trace("attachment", attachment, LogPrefix.ATTACHMENT);
378
379
return attachment;
380
};
381
382
return attachmentsToUpload.map(uploadAttachment);
383
};
384
385
const updateContent = async (
386
user: User,
387
publishFiles: PublishFiles,
388
id: string,
389
body: ContentBody,
390
titleToUpdate: string = title,
391
fileName: string = "",
392
uploadFileAttachments: boolean = true,
393
): Promise<PublishContentResult> => {
394
const previousPage = await client.getContent(id);
395
396
const attachmentsToUpload: string[] = findAttachments(
397
body.storage.value,
398
publishFiles.files,
399
fileName,
400
);
401
402
let uniqueTitle = titleToUpdate;
403
404
if (previousPage.title !== titleToUpdate) {
405
uniqueTitle = await uniquifyTitle(titleToUpdate, id);
406
}
407
408
trace("attachmentsToUpload", attachmentsToUpload, LogPrefix.ATTACHMENT);
409
410
const updatedBody: ContentBody = updateImagePaths(body);
411
updatedBody.storage.value = footnoteTransform(updatedBody.storage.value);
412
413
const toUpdate: ContentUpdate = {
414
contentChangeType: ContentChangeType.update,
415
id,
416
version: getNextVersion(previousPage),
417
title: uniqueTitle,
418
type: PAGE_TYPE,
419
status: ContentStatusEnum.current,
420
ancestors: null,
421
body: updatedBody,
422
};
423
424
trace("updateContent", toUpdate);
425
trace("updateContent body", toUpdate?.body?.storage?.value);
426
427
const updatedContent: Content = await client.updateContent(user, toUpdate);
428
429
if (toUpdate.id && uploadFileAttachments) {
430
const existingAttachments: AttachmentSummary[] = await client
431
.getAttachments(toUpdate.id);
432
433
trace(
434
"attachments",
435
{ existingAttachments, attachmentsToUpload },
436
LogPrefix.ATTACHMENT,
437
);
438
439
const uploadAttachmentsResult: (AttachmentSummary | null)[] = [];
440
441
for (let i = 0; i < attachmentsToUpload.length; i++) {
442
// Start exactly ONE upload by calling the helper with a single attachment
443
const tasks = uploadAttachments(
444
publishFiles.baseDir,
445
[attachmentsToUpload[i]], // <-- one at a time
446
toUpdate.id,
447
fileName,
448
existingAttachments,
449
);
450
451
const res = await tasks[0];
452
uploadAttachmentsResult.push(res);
453
454
if (i < attachmentsToUpload.length - 1) {
455
await sleep(ATTACHMENT_UPLOAD_DELAY_MS);
456
}
457
}
458
trace(
459
"uploadAttachmentsResult",
460
uploadAttachmentsResult,
461
LogPrefix.ATTACHMENT,
462
);
463
}
464
465
return {
466
content: updatedContent,
467
hasAttachments: attachmentsToUpload.length > 0,
468
};
469
};
470
471
const createSiteParent = async (
472
title: string,
473
body: ContentBody,
474
): Promise<Content> => {
475
let ancestors: ContentAncestor[] = [];
476
477
if (parent?.parent) {
478
ancestors = [{ id: parent.parent }];
479
} else if (space.homepage?.id) {
480
ancestors = [{ id: space.homepage?.id }];
481
}
482
483
const toCreate: ContentCreate = {
484
contentChangeType: ContentChangeType.create,
485
title,
486
type: PAGE_TYPE,
487
space,
488
status: ContentStatusEnum.current,
489
ancestors,
490
body,
491
};
492
493
const createdContent = await client.createContent(user, toCreate);
494
return createdContent;
495
};
496
497
const checkToCreateSiteParent = async (
498
parentId: string = "",
499
): Promise<string> => {
500
let isQuartoSiteParent = false;
501
502
const existingSiteParent: any = await client.getContent(parentId);
503
504
if (existingSiteParent?.id) {
505
const siteParentContentProperties: ContentProperty[] = await client
506
.getContentProperty(existingSiteParent.id ?? "");
507
508
isQuartoSiteParent = siteParentContentProperties.find(
509
(property: ContentProperty) =>
510
property.key === ContentPropertyKey.isQuartoSiteParent,
511
) !== undefined;
512
}
513
514
if (!isQuartoSiteParent) {
515
const body: ContentBody = {
516
storage: {
517
value: "",
518
representation: ContentBodyRepresentation.storage,
519
},
520
};
521
522
const siteParentTitle = await uniquifyTitle(title);
523
const siteParent: ContentSummary = await createSiteParent(
524
siteParentTitle,
525
body,
526
);
527
528
const newSiteParentId: string = siteParent.id ?? "";
529
530
const contentProperty: Content = await client.createContentProperty(
531
newSiteParentId,
532
{ key: ContentPropertyKey.isQuartoSiteParent, value: true },
533
);
534
535
parentId = newSiteParentId;
536
}
537
return parentId;
538
};
539
540
const createContent = async (
541
publishFiles: PublishFiles,
542
body: ContentBody,
543
titleToCreate: string = title,
544
createParent: ConfluenceParent = parent,
545
fileNameParam: string = "",
546
): Promise<PublishContentResult> => {
547
const createTitle = await uniquifyTitle(titleToCreate);
548
549
const fileName = pathWithForwardSlashes(fileNameParam);
550
551
const attachmentsToUpload: string[] = findAttachments(
552
body.storage.value,
553
publishFiles.files,
554
fileName,
555
);
556
557
trace("attachmentsToUpload", attachmentsToUpload, LogPrefix.ATTACHMENT);
558
const updatedBody: ContentBody = updateImagePaths(body);
559
updatedBody.storage.value = footnoteTransform(updatedBody.storage.value);
560
561
const toCreate: ContentCreate = {
562
contentChangeType: ContentChangeType.create,
563
title: createTitle,
564
type: PAGE_TYPE,
565
space,
566
status: ContentStatusEnum.current,
567
ancestors: createParent?.parent ? [{ id: createParent.parent }] : null,
568
body: updatedBody,
569
};
570
571
trace("createContent", { publishFiles, toCreate });
572
const createdContent = await client.createContent(user, toCreate);
573
574
if (createdContent.id) {
575
const uploadAttachmentsResult = await Promise.all(
576
uploadAttachments(
577
publishFiles.baseDir,
578
attachmentsToUpload,
579
createdContent.id,
580
fileName,
581
),
582
);
583
trace(
584
"uploadAttachmentsResult",
585
uploadAttachmentsResult,
586
LogPrefix.ATTACHMENT,
587
);
588
}
589
590
return {
591
content: createdContent,
592
hasAttachments: attachmentsToUpload.length > 0,
593
};
594
};
595
596
const publishDocument = async (): Promise<
597
[[PublishRecord, URL | undefined], boolean]
598
> => {
599
const publishFiles: PublishFiles = await renderDocument(render);
600
601
const body: ContentBody = loadDocument(
602
publishFiles.baseDir,
603
publishFiles.rootFile,
604
);
605
606
trace("publishDocument", { publishFiles, body }, LogPrefix.RENDER);
607
608
let publishResult: PublishContentResult | undefined;
609
let message: string = "";
610
let doOperation;
611
612
if (publishRecord) {
613
message = `Updating content at ${publishRecord.url}...`;
614
doOperation = async () => {
615
const result = await updateContent(
616
user,
617
publishFiles,
618
publishRecord.id,
619
body,
620
);
621
publishResult = result;
622
};
623
} else {
624
message = `Creating content in space ${parent.space}...`;
625
doOperation = async () => {
626
const result = await createContent(publishFiles, body);
627
publishResult = result;
628
};
629
}
630
try {
631
await doWithSpinner(message, doOperation);
632
return [
633
buildPublishRecordForContent(server, publishResult?.content),
634
!!publishResult?.hasAttachments,
635
];
636
} catch (error: any) {
637
trace("Error Performing Operation", error);
638
trace("Value to Update", body?.storage?.value);
639
throw error;
640
}
641
};
642
643
const publishSite = async (): Promise<
644
[[PublishRecord, URL | undefined], boolean]
645
> => {
646
let parentId: string = parent?.parent ?? space.homepage.id ?? "";
647
648
parentId = await checkToCreateSiteParent(parentId);
649
650
const siteParent: ConfluenceParent = {
651
space: parent.space,
652
parent: parentId,
653
};
654
655
let existingSite: SitePage[] = await fetchExistingSite(parentId);
656
trace("existingSite", existingSite);
657
658
const publishFiles: PublishFiles = await renderSite(render);
659
const metadataByInput: Record<string, InputMetadata> =
660
publishFiles.metadataByInput ?? {};
661
662
trace("metadataByInput", metadataByInput);
663
664
trace("publishSite", {
665
parentId,
666
publishFiles,
667
});
668
669
const filteredFiles: string[] = filterFilesForUpdate(publishFiles.files);
670
671
trace("filteredFiles", filteredFiles);
672
673
const assembleSiteFileMetadata = async (
674
fileName: string,
675
): Promise<SiteFileMetadata> => {
676
const fileToContentBody = async (
677
fileName: string,
678
): Promise<ContentBody> => {
679
return loadDocument(publishFiles.baseDir, fileName);
680
};
681
682
const originalTitle = getTitle(fileName, metadataByInput);
683
const title = originalTitle;
684
685
return await {
686
fileName,
687
title,
688
originalTitle,
689
contentBody: await fileToContentBody(fileName),
690
};
691
};
692
693
const fileMetadata: SiteFileMetadata[] = await Promise.all(
694
filteredFiles.map(assembleSiteFileMetadata),
695
);
696
697
trace("fileMetadata", fileMetadata);
698
699
let metadataByFilename = buildFileToMetaTable(existingSite);
700
701
trace("metadataByFilename", metadataByFilename);
702
703
let changeList: ConfluenceSpaceChange[] = buildSpaceChanges(
704
fileMetadata,
705
siteParent,
706
space,
707
existingSite,
708
);
709
710
changeList = flattenIndexes(changeList, metadataByFilename, parentId);
711
712
const { pass1Changes, pass2Changes } = updateLinks(
713
metadataByFilename,
714
changeList,
715
server,
716
siteParent,
717
);
718
719
changeList = pass1Changes;
720
721
trace("changelist Pass 1", changeList);
722
723
let pathsToId: Record<string, string> = {}; // build from existing site
724
725
const handleChangeError = (
726
label: string,
727
currentChange: ConfluenceSpaceChange,
728
error: any,
729
) => {
730
if (isContentUpdate(currentChange) || isContentCreate(currentChange)) {
731
trace("currentChange.fileName", currentChange.fileName);
732
trace("Value to Update", currentChange.body.storage.value);
733
}
734
if (EXIT_ON_ERROR) {
735
throw error;
736
}
737
};
738
let hasAttachments = false;
739
740
const doChange = async (
741
change: ConfluenceSpaceChange,
742
uploadFileAttachments: boolean = true,
743
) => {
744
if (isContentCreate(change)) {
745
if (change.fileName === "sitemap.xml") {
746
trace("sitemap.xml skipped", change);
747
return;
748
}
749
750
let ancestorId = (change?.ancestors && change?.ancestors[0]?.id) ??
751
null;
752
753
if (ancestorId && pathsToId[ancestorId]) {
754
ancestorId = pathsToId[ancestorId];
755
}
756
757
const ancestorParent: ConfluenceParent = {
758
space: parent.space,
759
parent: ancestorId ?? siteParent.parent,
760
};
761
762
const universalPath = pathWithForwardSlashes(change.fileName ?? "");
763
764
const result = await createContent(
765
publishFiles,
766
change.body,
767
change.title ?? "",
768
ancestorParent,
769
universalPath,
770
);
771
772
if (universalPath) {
773
pathsToId[universalPath] = result.content.id ?? "";
774
}
775
776
const contentPropertyResult: Content = await client
777
.createContentProperty(result.content.id ?? "", {
778
key: ContentPropertyKey.fileName,
779
value: (change as ContentCreate).fileName,
780
});
781
hasAttachments = hasAttachments || result.hasAttachments;
782
return result;
783
} else if (isContentUpdate(change)) {
784
const update = change as ContentUpdate;
785
const result = await updateContent(
786
user,
787
publishFiles,
788
update.id ?? "",
789
update.body,
790
update.title ?? "",
791
update.fileName ?? "",
792
uploadFileAttachments,
793
);
794
hasAttachments = hasAttachments || result.hasAttachments;
795
return result;
796
} else if (isContentDelete(change)) {
797
if (DELETE_DISABLED) {
798
console.warn("DELETE DISABELD");
799
return null;
800
}
801
const result = await client.deleteContent(change);
802
await sleep(DELETE_SLEEP_MILLIS); // TODO replace with polling
803
return { content: result, hasAttachments: false };
804
} else {
805
console.error("Space Change not defined");
806
return null;
807
}
808
};
809
810
let pass1Count = 0;
811
for (let currentChange of changeList) {
812
try {
813
pass1Count = pass1Count + 1;
814
const doOperation = async () => await doChange(currentChange);
815
await doWithSpinner(
816
`Site Updates [${pass1Count}/${changeList.length}]`,
817
doOperation,
818
);
819
} catch (error: any) {
820
handleChangeError(
821
"Error Performing Change Pass 1",
822
currentChange,
823
error,
824
);
825
}
826
}
827
828
if (pass2Changes.length) {
829
//PASS #2 to update links to newly created pages
830
831
trace("changelist Pass 2", pass2Changes);
832
833
existingSite = await fetchExistingSite(parentId);
834
metadataByFilename = buildFileToMetaTable(existingSite);
835
836
const linkUpdateChanges: ConfluenceSpaceChange[] = convertForSecondPass(
837
metadataByFilename,
838
pass2Changes,
839
server,
840
parent,
841
);
842
843
let pass2Count = 0;
844
for (let currentChange of linkUpdateChanges) {
845
try {
846
pass2Count = pass2Count + 1;
847
const doOperation = async () => await doChange(currentChange, false);
848
await doWithSpinner(
849
`Updating Links [${pass2Count}/${linkUpdateChanges.length}]`,
850
doOperation,
851
);
852
} catch (error: any) {
853
handleChangeError(
854
"Error Performing Change Pass 2",
855
currentChange,
856
error,
857
);
858
}
859
}
860
}
861
862
const parentPage: Content = await client.getContent(parentId);
863
return [buildPublishRecordForContent(server, parentPage), hasAttachments];
864
};
865
866
if (type === PublishTypeEnum.document) {
867
const [publishResult, hasAttachments] = await publishDocument();
868
if (hasAttachments) {
869
info(
870
"\nNote: The published content includes attachments or images. You may see a placeholder for a few moments while Confluence processes the image or attachment.\n",
871
);
872
}
873
return publishResult;
874
} else {
875
const [publishResult, hasAttachments] = await publishSite();
876
if (hasAttachments) {
877
info(
878
"\nNote: The published content includes attachments or images. You may see a placeholder for a few moments while Confluence processes the image or attachment.\n",
879
);
880
}
881
return publishResult;
882
}
883
}
884
885
export const confluenceProvider: PublishProvider = {
886
name: CONFLUENCE_ID,
887
description: "Confluence",
888
hidden: false,
889
requiresServer: true,
890
requiresRender: true,
891
accountTokens: getAccountTokens,
892
authorizeToken: promptAndAuthorizeToken,
893
removeToken,
894
resolveTarget,
895
publish,
896
isUnauthorized,
897
isNotFound,
898
};
899
900