feat: Backup & Restore - Manual backup tested complete.
This commit is contained in:
319
backend/src/features/backup/api/backup.controller.ts
Normal file
319
backend/src/features/backup/api/backup.controller.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* @ai-summary Controller for backup API endpoints
|
||||
* @ai-context Handles HTTP requests for backup operations
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { Pool } from 'pg';
|
||||
import { BackupService } from '../domain/backup.service';
|
||||
import { BackupRestoreService } from '../domain/backup-restore.service';
|
||||
import {
|
||||
ListBackupsQuery,
|
||||
CreateBackupBody,
|
||||
BackupIdParam,
|
||||
RestoreBody,
|
||||
CreateScheduleBody,
|
||||
UpdateScheduleBody,
|
||||
ScheduleIdParam,
|
||||
UpdateSettingsBody,
|
||||
} from './backup.validation';
|
||||
|
||||
export class BackupController {
|
||||
private backupService: BackupService;
|
||||
private restoreService: BackupRestoreService;
|
||||
|
||||
constructor(pool: Pool) {
|
||||
this.backupService = new BackupService(pool);
|
||||
this.restoreService = new BackupRestoreService(pool);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Backup Operations
|
||||
// ============================================
|
||||
|
||||
async listBackups(
|
||||
request: FastifyRequest<{ Querystring: ListBackupsQuery }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const result = await this.backupService.listBackups(request.query);
|
||||
reply.send(result);
|
||||
}
|
||||
|
||||
async createBackup(
|
||||
request: FastifyRequest<{ Body: CreateBackupBody }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const adminSub = (request as any).userContext?.auth0Sub;
|
||||
|
||||
const result = await this.backupService.createBackup({
|
||||
name: request.body.name,
|
||||
backupType: 'manual',
|
||||
createdBy: adminSub,
|
||||
includeDocuments: request.body.includeDocuments,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
reply.status(201).send({
|
||||
backupId: result.backupId,
|
||||
status: 'completed',
|
||||
message: 'Backup created successfully',
|
||||
});
|
||||
} else {
|
||||
reply.status(500).send({
|
||||
backupId: result.backupId,
|
||||
status: 'failed',
|
||||
message: result.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getBackup(
|
||||
request: FastifyRequest<{ Params: BackupIdParam }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const backup = await this.backupService.getBackup(request.params.id);
|
||||
|
||||
if (!backup) {
|
||||
reply.status(404).send({ error: 'Backup not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
reply.send(backup);
|
||||
}
|
||||
|
||||
async downloadBackup(
|
||||
request: FastifyRequest<{ Params: BackupIdParam }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const download = await this.backupService.createDownloadStream(request.params.id);
|
||||
|
||||
if (!download) {
|
||||
reply.status(404).send({ error: 'Backup not found or file not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
return reply
|
||||
.header('Content-Type', 'application/gzip')
|
||||
.header('Content-Disposition', `attachment; filename="${download.filename}"`)
|
||||
.header('Content-Length', download.size)
|
||||
.send(download.stream);
|
||||
}
|
||||
|
||||
async deleteBackup(
|
||||
request: FastifyRequest<{ Params: BackupIdParam }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const deleted = await this.backupService.deleteBackup(request.params.id);
|
||||
|
||||
if (!deleted) {
|
||||
reply.status(404).send({ error: 'Backup not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
reply.status(204).send();
|
||||
}
|
||||
|
||||
async uploadBackup(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const adminSub = (request as any).userContext?.auth0Sub;
|
||||
|
||||
// Handle multipart file upload
|
||||
const data = await request.file();
|
||||
if (!data) {
|
||||
reply.status(400).send({ error: 'No file uploaded' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const filename = data.filename;
|
||||
if (!filename.endsWith('.tar.gz') && !filename.endsWith('.tgz')) {
|
||||
reply.status(400).send({ error: 'Invalid file type. Expected .tar.gz archive' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Save file temporarily
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const os = await import('os');
|
||||
|
||||
const tempPath = path.join(os.tmpdir(), `upload-${Date.now()}-${filename}`);
|
||||
const writeStream = fs.createWriteStream(tempPath);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
data.file.pipe(writeStream);
|
||||
data.file.on('end', resolve);
|
||||
data.file.on('error', reject);
|
||||
});
|
||||
|
||||
try {
|
||||
const backup = await this.backupService.importUploadedBackup(
|
||||
tempPath,
|
||||
filename,
|
||||
adminSub
|
||||
);
|
||||
|
||||
reply.status(201).send({
|
||||
backupId: backup.id,
|
||||
filename: backup.filename,
|
||||
fileSizeBytes: backup.fileSizeBytes,
|
||||
message: 'Backup uploaded successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
// Cleanup temp file on error
|
||||
await fs.promises.unlink(tempPath).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Restore Operations
|
||||
// ============================================
|
||||
|
||||
async previewRestore(
|
||||
request: FastifyRequest<{ Params: BackupIdParam }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
try {
|
||||
const preview = await this.restoreService.previewRestore(request.params.id);
|
||||
reply.send(preview);
|
||||
} catch (error) {
|
||||
reply.status(400).send({
|
||||
error: error instanceof Error ? error.message : 'Failed to preview restore',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async executeRestore(
|
||||
request: FastifyRequest<{ Params: BackupIdParam; Body: RestoreBody }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
try {
|
||||
const result = await this.restoreService.executeRestore({
|
||||
backupId: request.params.id,
|
||||
createSafetyBackup: request.body.createSafetyBackup,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
reply.send({
|
||||
success: true,
|
||||
safetyBackupId: result.safetyBackupId,
|
||||
restoredAt: new Date().toISOString(),
|
||||
message: 'Restore completed successfully',
|
||||
});
|
||||
} else {
|
||||
reply.status(500).send({
|
||||
success: false,
|
||||
safetyBackupId: result.safetyBackupId,
|
||||
error: result.error,
|
||||
message: 'Restore failed',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
reply.status(400).send({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to execute restore',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getRestoreStatus(
|
||||
_request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const status = this.restoreService.getRestoreStatus();
|
||||
reply.send(status);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Schedule Operations
|
||||
// ============================================
|
||||
|
||||
async listSchedules(
|
||||
_request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const schedules = await this.backupService.listSchedules();
|
||||
reply.send(schedules);
|
||||
}
|
||||
|
||||
async createSchedule(
|
||||
request: FastifyRequest<{ Body: CreateScheduleBody }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const schedule = await this.backupService.createSchedule(
|
||||
request.body.name,
|
||||
request.body.frequency,
|
||||
request.body.retentionCount,
|
||||
request.body.isEnabled
|
||||
);
|
||||
|
||||
reply.status(201).send(schedule);
|
||||
}
|
||||
|
||||
async updateSchedule(
|
||||
request: FastifyRequest<{ Params: ScheduleIdParam; Body: UpdateScheduleBody }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const schedule = await this.backupService.updateSchedule(
|
||||
request.params.id,
|
||||
request.body
|
||||
);
|
||||
|
||||
if (!schedule) {
|
||||
reply.status(404).send({ error: 'Schedule not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
reply.send(schedule);
|
||||
}
|
||||
|
||||
async deleteSchedule(
|
||||
request: FastifyRequest<{ Params: ScheduleIdParam }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const deleted = await this.backupService.deleteSchedule(request.params.id);
|
||||
|
||||
if (!deleted) {
|
||||
reply.status(404).send({ error: 'Schedule not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
reply.status(204).send();
|
||||
}
|
||||
|
||||
async toggleSchedule(
|
||||
request: FastifyRequest<{ Params: ScheduleIdParam }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const schedule = await this.backupService.toggleSchedule(request.params.id);
|
||||
|
||||
if (!schedule) {
|
||||
reply.status(404).send({ error: 'Schedule not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
reply.send(schedule);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Settings Operations
|
||||
// ============================================
|
||||
|
||||
async getSettings(
|
||||
_request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const settings = await this.backupService.getSettings();
|
||||
reply.send(settings);
|
||||
}
|
||||
|
||||
async updateSettings(
|
||||
request: FastifyRequest<{ Body: UpdateSettingsBody }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const settings = await this.backupService.updateSettings(request.body);
|
||||
reply.send(settings);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user