feat: Backend-Modul fuer vollstaendiges Daten-Backup (Export/Import)
Neues Modul src/backup/ fuer Backups und Datenuebertragung zwischen Deployments: GET /backup/export liefert ein JSON-Bundle mit allen fachlichen Daten (Bienenstaende bis Etikettenvorlagen, ~30 Tabellen), POST /backup/import spielt so ein Bundle wieder ein. Bewusst NICHT enthalten: User/Rollen/Rechte (werden ueber die Benutzerverwaltung gepflegt, Permission-Zeilen werden ohnehin bei jedem Start aus dem Code neu synchronisiert), Audit-Log (unveraenderliche Historie), Notifications (fluechtig), Dokumente (nur die DB-Zeile waere gesichert, nicht die tatsaechliche Datei auf der Festplatte). Import laeuft in einer Transaktion, Tabellen in Abhaengigkeitsreihenfolge (z.B. Bienenstand vor Volk, Volk vor Durchsicht). Selbstreferenzen (Koenigin -> Mutterkoenigin) werden in zwei Durchgaengen aufgeloest, damit die Reihenfolge im Export keine Rolle spielt. userId-Felder, die im Zielsystem nicht existieren (z.B. Uebertragung auf ein anderes Deployment), werden auf den ausfuehrenden Admin umgebogen statt die Fremdschluessel-Pruefung scheitern zu lassen. AutomationRule wird ueber triggerType statt id abgeglichen, da diese Zeilen code-fest sind. Neue Berechtigungen backup:export/backup:import. 8 neue Unit-Tests fuer die Sonderfaelle (Selbstreferenz, userId-Remapping, AutomationRule-Abgleich, Audit-Eintrag). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
390ec32f50
commit
2f223b6941
6 files changed
+896
No files matched your search
@@ -0,0 +1,73 @@
|
||||
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';
|
||||
|
||||
// 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) {}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user