Files
docmost/apps/server/src/integrations/export/export.controller.ts
T
2026-04-27 15:16:26 +01:00

163 lines
4.4 KiB
TypeScript

import {
Body,
Controller,
ForbiddenException,
HttpCode,
HttpStatus,
Inject,
NotFoundException,
Post,
Res,
UseGuards,
} from '@nestjs/common';
import { ExportService } from './export.service';
import { ExportPageDto, ExportSpaceDto } from './dto/export-dto';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { User } from '@docmost/db/types/entity.types';
import SpaceAbilityFactory from '../../core/casl/abilities/space-ability.factory';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { PageAccessService } from '../../core/page/page-access/page-access.service';
import {
SpaceCaslAction,
SpaceCaslSubject,
} from '../../core/casl/interfaces/space-ability.type';
import { FastifyReply } from 'fastify';
import { getExportExtension } from './utils';
import {
getMimeType,
getPageTitle,
sanitizeFileName,
} from '../../common/helpers';
import * as path from 'path';
import { AuditEvent, AuditResource } from '../../common/events/audit-events';
import {
AUDIT_SERVICE,
IAuditService,
} from '../../integrations/audit/audit.service';
@Controller()
export class ExportController {
constructor(
private readonly exportService: ExportService,
private readonly pageRepo: PageRepo,
private readonly spaceAbility: SpaceAbilityFactory,
private readonly pageAccessService: PageAccessService,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
) {}
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('pages/export')
async exportPage(
@Body() dto: ExportPageDto,
@AuthUser() user: User,
@Res() res: FastifyReply,
) {
const page = await this.pageRepo.findById(dto.pageId, {
includeContent: true,
});
if (!page || page.deletedAt) {
throw new NotFoundException('Page not found');
}
await this.pageAccessService.validateCanView(page, user);
const result = await this.exportService.exportPages(
dto.pageId,
dto.format,
dto.includeAttachments,
dto.includeChildren,
user.id,
);
this.auditService.log({
event: AuditEvent.PAGE_EXPORTED,
resourceType: AuditResource.PAGE,
resourceId: page.id,
spaceId: page.spaceId,
metadata: {
title: getPageTitle(page.title),
format: dto.format,
includeChildren: dto.includeChildren,
includeAttachments: dto.includeAttachments,
spaceId: page.spaceId,
},
});
if (result.type === 'file') {
const ext = getExportExtension(dto.format);
const fileName =
sanitizeFileName(page.title || 'untitled', { preserveSpaces: true }) +
ext;
const contentType = getMimeType(path.extname(fileName));
res.headers({
'Content-Type': contentType,
'Content-Disposition':
'attachment; filename="' + encodeURIComponent(fileName) + '"',
});
res.send(result.content);
} else {
const fileName =
sanitizeFileName(page.title || 'untitled', { preserveSpaces: true }) +
'.zip';
res.headers({
'Content-Type': 'application/zip',
'Content-Disposition':
'attachment; filename="' + encodeURIComponent(fileName) + '"',
});
res.send(result.stream);
}
}
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('spaces/export')
async exportSpace(
@Body() dto: ExportSpaceDto,
@AuthUser() user: User,
@Res() res: FastifyReply,
) {
const ability = await this.spaceAbility.createForUser(user, dto.spaceId);
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)) {
throw new ForbiddenException();
}
const exportFile = await this.exportService.exportSpace(
dto.spaceId,
dto.format,
dto.includeAttachments,
user.id,
);
this.auditService.log({
event: AuditEvent.SPACE_EXPORTED,
resourceType: AuditResource.SPACE,
resourceId: dto.spaceId,
spaceId: dto.spaceId,
metadata: {
format: dto.format,
includeAttachments: dto.includeAttachments ?? false,
spaceName: exportFile.spaceName,
},
});
res.headers({
'Content-Type': 'application/zip',
'Content-Disposition':
'attachment; filename="' +
encodeURIComponent(
sanitizeFileName(exportFile.fileName, { preserveSpaces: true }),
) +
'"',
});
res.send(exportFile.fileStream);
}
}