Files
ApiCore/ApiCore-Backend/src/backup/backup.controller.ts
T
Nick Szaunig 7b23862a41
Backend Image / build-and-push (push) Successful in 2m16s
Frontend Image / build-and-push (push) Successful in 1m56s
feat: expand backup to all data and add automatic scheduled backups
Backup export/import now covers Role, User, RolePermission,
Notification, AuditLog and Document (with actual file content), not
just business data - matching "wirklich alle Daten". Role/User use
natural keys (role.key, resource+action) so a bundle restores cleanly
into a different database.

Adds BackupSchedulerService (@nestjs/schedule) for cron-based automatic
backups to a local path (BACKUP_STORAGE_PATH), with retention pruning
and a file-mtime guard against duplicate runs across replicas. New
GET /backup/schedule and POST /backup/schedule/run-now endpoints, plus
a status card and manual trigger in the frontend Backup settings page.
2026-09-03 11:33:48 +02:00

93 lines
3.0 KiB
TypeScript

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<BackupScheduleStatus> {
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<void> {
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);
}
}