import { BadRequestException, Controller, Get, Post, Res, UploadedFile, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiConsumes, ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; import { RequirePermission } from '../auth/decorators/require-permission.decorator'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; import type { AuthenticatedUser } from '../auth/types/authenticated-user'; import { BackupBundle, BackupService } from './backup.service'; import { BackupScheduleStatus, BackupSchedulerService, } from './backup-scheduler.service'; // A full backup bundle can be much larger than the 5mb CSV import cap // elsewhere (dozens of tables, potentially years of records, plus embedded // label-template images) - 50mb comfortably covers a real dataset without // raising the app-wide JSON body limit for every other route. const MAX_BACKUP_FILE_SIZE_BYTES = 50 * 1024 * 1024; @ApiTags('backup') @ApiBearerAuth() @Controller('backup') export class BackupController { constructor( private readonly backupService: BackupService, private readonly backupSchedulerService: BackupSchedulerService, ) {} @RequirePermission('backup:export') @Get('schedule') getSchedule(): Promise { return this.backupSchedulerService.getStatus(); } @RequirePermission('backup:export') @Post('schedule/run-now') runScheduleNow(): Promise<{ filePath: string }> { return this.backupSchedulerService.runNow(); } @RequirePermission('backup:export') @Get('export') async export(@Res() res: Response): Promise { const bundle = await this.backupService.exportAll(); const filename = `apicore-backup-${new Date().toISOString().slice(0, 10)}.json`; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.send(JSON.stringify(bundle, null, 2)); } @RequirePermission('backup:import') @Post('import') @ApiConsumes('multipart/form-data') @UseInterceptors( FileInterceptor('file', { limits: { fileSize: MAX_BACKUP_FILE_SIZE_BYTES }, fileFilter: (_req, file, callback) => { if (!file.originalname.toLowerCase().endsWith('.json')) { callback( new BadRequestException('Nur JSON-Dateien werden unterstützt'), false, ); return; } callback(null, true); }, }), ) async import( @UploadedFile() file: Express.Multer.File | undefined, @CurrentUser() actor: AuthenticatedUser, ) { if (!file) { throw new BadRequestException('No file uploaded'); } let bundle: BackupBundle; try { bundle = JSON.parse(file.buffer.toString('utf-8')) as BackupBundle; } catch { throw new BadRequestException('Datei ist kein gültiges JSON'); } return this.backupService.importAll(bundle, actor.id); } }