fix: tighten export

This commit is contained in:
Philipinho
2026-04-21 14:14:30 +01:00
parent 51a019c5c9
commit 6c1bb2494c
@@ -39,6 +39,8 @@ import {
} from '../../common/helpers/prosemirror/utils'; } from '../../common/helpers/prosemirror/utils';
import { htmlToMarkdown } from '@docmost/editor-ext'; import { htmlToMarkdown } from '@docmost/editor-ext';
type AllowedAttachment = { id: string; fileName: string; filePath: string };
@Injectable() @Injectable()
export class ExportService { export class ExportService {
private readonly logger = new Logger(ExportService.name); private readonly logger = new Logger(ExportService.name);
@@ -272,6 +274,12 @@ export class ExportService {
computeLocalPath(tree, format, null, '', slugIdToPath); computeLocalPath(tree, format, null, '', slugIdToPath);
// Batch resolve attachments once for the whole export so we only run the
// owning-page view check a single time, regardless of page count.
const allowedAttachments = includeAttachments
? await this.resolveAccessibleAttachments(tree, userId, ignorePermissions)
: new Map<string, AllowedAttachment>();
const stack: { folder: JSZip; parentPageId: string | null }[] = [ const stack: { folder: JSZip; parentPageId: string | null }[] = [
{ folder: zip, parentPageId: null }, { folder: zip, parentPageId: null },
]; ];
@@ -301,7 +309,7 @@ export class ExportService {
); );
if (includeAttachments) { if (includeAttachments) {
await this.zipAttachments(updatedJsonContent, page.spaceId, folder); await this.zipAttachments(updatedJsonContent, folder, allowedAttachments);
updatedJsonContent = updatedJsonContent =
updateAttachmentUrlsToLocalPaths(updatedJsonContent); updateAttachmentUrlsToLocalPaths(updatedJsonContent);
} }
@@ -347,31 +355,80 @@ export class ExportService {
zip.file('docmost-metadata.json', JSON.stringify(metadata, null, 2)); zip.file('docmost-metadata.json', JSON.stringify(metadata, null, 2));
} }
async zipAttachments(prosemirrorJson: any, spaceId: string, zip: JSZip) { async zipAttachments(
prosemirrorJson: any,
zip: JSZip,
allowed: Map<string, AllowedAttachment>,
) {
const attachmentIds = getAttachmentIds(prosemirrorJson); const attachmentIds = getAttachmentIds(prosemirrorJson);
if (attachmentIds.length > 0) { await Promise.all(
const attachments = await this.db attachmentIds.map(async (id) => {
.selectFrom('attachments') const attachment = allowed.get(id);
.select(['id', 'fileName', 'filePath']) if (!attachment) return;
.where('id', 'in', attachmentIds) try {
.where('spaceId', '=', spaceId) const fileBuffer = await this.storageService.read(
.execute(); attachment.filePath,
);
const filePath = `/files/${attachment.id}/${attachment.fileName}`;
zip.file(filePath, fileBuffer);
} catch (err) {
this.logger.debug(`Attachment export error ${attachment.id}`, err);
}
}),
);
}
await Promise.all( private async resolveAccessibleAttachments(
attachments.map(async (attachment) => { tree: PageExportTree,
try { userId: string | undefined,
const fileBuffer = await this.storageService.read( ignorePermissions: boolean,
attachment.filePath, ): Promise<Map<string, AllowedAttachment>> {
); const allAttachmentIds = new Set<string>();
const filePath = `/files/${attachment.id}/${attachment.fileName}`; let spaceId: string | undefined;
zip.file(filePath, fileBuffer); for (const siblings of Object.values(tree)) {
} catch (err) { for (const page of siblings) {
this.logger.debug(`Attachment export error ${attachment.id}`, err); if (!spaceId) spaceId = page.spaceId;
} for (const id of getAttachmentIds(getProsemirrorContent(page.content))) {
}), allAttachmentIds.add(id);
}
}
}
if (allAttachmentIds.size === 0 || !spaceId) {
return new Map();
}
const attachments = await this.db
.selectFrom('attachments')
.select(['id', 'fileName', 'filePath', 'pageId'])
.where('id', 'in', [...allAttachmentIds])
.where('spaceId', '=', spaceId)
.execute();
let visible = attachments;
if (!ignorePermissions && userId) {
const ownerPageIds = [
...new Set(
attachments
.map((a) => a.pageId)
.filter((id): id is string => !!id),
),
];
const accessible = ownerPageIds.length
? await this.pagePermissionRepo.filterAccessiblePageIds({
pageIds: ownerPageIds,
userId,
spaceId,
})
: [];
const accessibleSet = new Set(accessible);
visible = attachments.filter(
(a) => a.pageId && accessibleSet.has(a.pageId),
); );
} }
return new Map(visible.map((a) => [a.id, a]));
} }
async turnPageMentionsToLinks( async turnPageMentionsToLinks(