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.
93 lines
3.0 KiB
TypeScript
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);
|
|
}
|
|
}
|