feat(backup): add containerarchive-backed backup storage, restore, download, and pruning support

This commit is contained in:
2026-03-24 19:54:56 +00:00
parent 22a7e76645
commit 0799efadae
18 changed files with 816 additions and 447 deletions

View File

@@ -2161,27 +2161,47 @@ export class OneboxHttpServer {
*/
private async handleDownloadBackupRequest(backupId: number): Promise<Response> {
try {
const filePath = this.oneboxRef.backupManager.getBackupFilePath(backupId);
if (!filePath) {
const backup = this.oneboxRef.database.getBackupById(backupId);
if (!backup) {
return this.jsonResponse({ success: false, error: 'Backup not found' }, 404);
}
let downloadPath: string | null = null;
let tempExport = false;
if (backup.snapshotId) {
// ContainerArchive backup: export as encrypted tar
downloadPath = await this.oneboxRef.backupManager.getBackupExportPath(backupId);
tempExport = true;
} else {
// Legacy file-based backup
downloadPath = this.oneboxRef.backupManager.getBackupFilePath(backupId);
}
if (!downloadPath) {
return this.jsonResponse({ success: false, error: 'Backup file not available' }, 404);
}
// Check if file exists
try {
await Deno.stat(filePath);
await Deno.stat(downloadPath);
} catch {
return this.jsonResponse({ success: false, error: 'Backup file not found on disk' }, 404);
}
// Read file and return as download
const backup = this.oneboxRef.database.getBackupById(backupId);
const file = await Deno.readFile(filePath);
const file = await Deno.readFile(downloadPath);
const filename = backup.filename || `${backup.serviceName}-${backup.createdAt}.tar.enc`;
// Clean up temp export file
if (tempExport) {
try { await Deno.remove(downloadPath); } catch { /* ignore */ }
}
return new Response(file, {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${backup?.filename || 'backup.tar.enc'}"`,
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': String(file.length),
},
});
@@ -2241,12 +2261,6 @@ export class OneboxHttpServer {
}, 400);
}
// Get backup file path
const filePath = this.oneboxRef.backupManager.getBackupFilePath(backupId);
if (!filePath) {
return this.jsonResponse({ success: false, error: 'Backup not found' }, 404);
}
// Validate mode-specific requirements
if ((mode === 'import' || mode === 'clone') && !newServiceName) {
return this.jsonResponse({
@@ -2255,7 +2269,7 @@ export class OneboxHttpServer {
}, 400);
}
const result = await this.oneboxRef.backupManager.restoreBackup(filePath, {
const result = await this.oneboxRef.backupManager.restoreBackup(backupId, {
mode,
newServiceName,
overwriteExisting: overwriteExisting === true,