feat: add attachments support for single page exports (#1440)

* feat: add attachments support for single page exports
- Add includeAttachments option to page export modal and API
- Fix internal page url in single page exports in cloud

* remove redundant line

* preserve export state
This commit is contained in:
Philip Okugbe
2025-08-04 08:01:11 +01:00
committed by GitHub
parent aa6eec754e
commit dddfd48934
6 changed files with 58 additions and 142 deletions

View File

@ -23,6 +23,10 @@ export class ExportPageDto {
@IsOptional()
@IsBoolean()
includeChildren?: boolean;
@IsOptional()
@IsBoolean()
includeAttachments?: boolean;
}
export class ExportSpaceDto {

View File

@ -55,40 +55,22 @@ export class ExportController {
throw new ForbiddenException();
}
const fileExt = getExportExtension(dto.format);
const fileName = sanitize(page.title || 'untitled') + fileExt;
if (dto.includeChildren) {
const zipFileBuffer = await this.exportService.exportPageWithChildren(
dto.pageId,
dto.format,
);
const newName = path.parse(fileName).name + '.zip';
res.headers({
'Content-Type': 'application/zip',
'Content-Disposition':
'attachment; filename="' + encodeURIComponent(newName) + '"',
});
res.send(zipFileBuffer);
return;
}
const rawContent = await this.exportService.exportPage(
const zipFileBuffer = await this.exportService.exportPages(
dto.pageId,
dto.format,
page,
true,
dto.includeAttachments,
dto.includeChildren,
);
const fileName = sanitize(page.title || 'untitled') + '.zip';
res.headers({
'Content-Type': getMimeType(fileExt),
'Content-Type': 'application/zip',
'Content-Disposition':
'attachment; filename="' + encodeURIComponent(fileName) + '"',
});
res.send(rawContent);
res.send(zipFileBuffer);
}
@UseGuards(JwtAuthGuard)

View File

@ -89,10 +89,28 @@ export class ExportService {
return;
}
async exportPageWithChildren(pageId: string, format: string) {
const pages = await this.pageRepo.getPageAndDescendants(pageId, {
includeContent: true,
});
async exportPages(
pageId: string,
format: string,
includeAttachments: boolean,
includeChildren: boolean,
) {
let pages: Page[];
if (includeChildren) {
//@ts-ignore
pages = await this.pageRepo.getPageAndDescendants(pageId, {
includeContent: true,
});
} else {
// Only fetch the single page when includeChildren is false
const page = await this.pageRepo.findById(pageId, {
includeContent: true,
});
if (page){
pages = [page];
}
}
if (!pages || pages.length === 0) {
throw new BadRequestException('No pages to export');
@ -105,7 +123,7 @@ export class ExportService {
const tree = buildTree(pages as Page[]);
const zip = new JSZip();
await this.zipPages(tree, format, zip);
await this.zipPages(tree, format, zip, includeAttachments);
const zipFile = zip.generateNodeStream({
type: 'nodebuffer',
@ -168,7 +186,7 @@ export class ExportService {
tree: PageExportTree,
format: string,
zip: JSZip,
includeAttachments = true,
includeAttachments: boolean,
): Promise<void> {
const slugIdToPath: Record<string, string> = {};
@ -200,7 +218,8 @@ export class ExportService {
if (includeAttachments) {
await this.zipAttachments(updatedJsonContent, page.spaceId, folder);
updatedJsonContent = updateAttachmentUrlsToLocalPaths(updatedJsonContent);
updatedJsonContent =
updateAttachmentUrlsToLocalPaths(updatedJsonContent);
}
const pageTitle = getPageTitle(page.title);