feat(backup): Add backup system: BackupManager, DB schema, API endpoints and UI support
Introduce a complete service backup/restore subsystem with encrypted archives, database records and REST endpoints. Implements BackupManager with export/import for service config, platform resources (MongoDB, MinIO, ClickHouse), and Docker images; adds BackupRepository and migrations for backups table and include_image_in_backup; integrates backup flows into the HTTP API and the UI client; exposes backup password management and restore modes (restore/import/clone). Wire BackupManager into Onebox initialization.
This commit is contained in:
@@ -18,6 +18,7 @@ import type {
|
||||
IDomain,
|
||||
ICertificate,
|
||||
ICertRequirement,
|
||||
IBackup,
|
||||
} from '../types.ts';
|
||||
import type { TBindValue } from './types.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
AuthRepository,
|
||||
MetricsRepository,
|
||||
PlatformRepository,
|
||||
BackupRepository,
|
||||
} from './repositories/index.ts';
|
||||
|
||||
export class OneboxDatabase {
|
||||
@@ -44,6 +46,7 @@ export class OneboxDatabase {
|
||||
private authRepo!: AuthRepository;
|
||||
private metricsRepo!: MetricsRepository;
|
||||
private platformRepo!: PlatformRepository;
|
||||
private backupRepo!: BackupRepository;
|
||||
|
||||
constructor(dbPath = './.nogit/onebox.db') {
|
||||
this.dbPath = dbPath;
|
||||
@@ -76,6 +79,7 @@ export class OneboxDatabase {
|
||||
this.authRepo = new AuthRepository(queryFn);
|
||||
this.metricsRepo = new MetricsRepository(queryFn);
|
||||
this.platformRepo = new PlatformRepository(queryFn);
|
||||
this.backupRepo = new BackupRepository(queryFn);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize database: ${getErrorMessage(error)}`);
|
||||
throw error;
|
||||
@@ -705,6 +709,37 @@ export class OneboxDatabase {
|
||||
this.setMigrationVersion(8);
|
||||
logger.success('Migration 8 completed: Certificates table now stores PEM content');
|
||||
}
|
||||
|
||||
// Migration 9: Backup system tables
|
||||
const version9 = this.getMigrationVersion();
|
||||
if (version9 < 9) {
|
||||
logger.info('Running migration 9: Creating backup system tables...');
|
||||
|
||||
// Add include_image_in_backup column to services table
|
||||
this.query(`ALTER TABLE services ADD COLUMN include_image_in_backup INTEGER DEFAULT 1`);
|
||||
|
||||
// Create backups table
|
||||
this.query(`
|
||||
CREATE TABLE backups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
service_id INTEGER NOT NULL,
|
||||
service_name TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
size_bytes INTEGER NOT NULL,
|
||||
created_at REAL NOT NULL,
|
||||
includes_image INTEGER NOT NULL,
|
||||
platform_resources TEXT NOT NULL DEFAULT '[]',
|
||||
checksum TEXT NOT NULL,
|
||||
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_backups_service ON backups(service_id)');
|
||||
this.query('CREATE INDEX IF NOT EXISTS idx_backups_created ON backups(created_at DESC)');
|
||||
|
||||
this.setMigrationVersion(9);
|
||||
logger.success('Migration 9 completed: Backup system tables created');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Migration failed: ${getErrorMessage(error)}`);
|
||||
if (error instanceof Error && error.stack) {
|
||||
@@ -1078,4 +1113,30 @@ export class OneboxDatabase {
|
||||
deletePlatformResourcesByService(serviceId: number): void {
|
||||
this.platformRepo.deletePlatformResourcesByService(serviceId);
|
||||
}
|
||||
|
||||
// ============ Backups (delegated to repository) ============
|
||||
|
||||
createBackup(backup: Omit<IBackup, 'id'>): IBackup {
|
||||
return this.backupRepo.create(backup);
|
||||
}
|
||||
|
||||
getBackupById(id: number): IBackup | null {
|
||||
return this.backupRepo.getById(id);
|
||||
}
|
||||
|
||||
getBackupsByService(serviceId: number): IBackup[] {
|
||||
return this.backupRepo.getByService(serviceId);
|
||||
}
|
||||
|
||||
getAllBackups(): IBackup[] {
|
||||
return this.backupRepo.getAll();
|
||||
}
|
||||
|
||||
deleteBackup(id: number): void {
|
||||
this.backupRepo.delete(id);
|
||||
}
|
||||
|
||||
deleteBackupsByService(serviceId: number): void {
|
||||
this.backupRepo.deleteByService(serviceId);
|
||||
}
|
||||
}
|
||||
|
||||
86
ts/database/repositories/backup.repository.ts
Normal file
86
ts/database/repositories/backup.repository.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Backup Repository
|
||||
* Handles CRUD operations for backups table
|
||||
*/
|
||||
|
||||
import { BaseRepository } from '../base.repository.ts';
|
||||
import type { IBackup, TPlatformServiceType } from '../../types.ts';
|
||||
|
||||
export class BackupRepository extends BaseRepository {
|
||||
create(backup: Omit<IBackup, 'id'>): IBackup {
|
||||
this.query(
|
||||
`INSERT INTO backups (
|
||||
service_id, service_name, filename, size_bytes, created_at,
|
||||
includes_image, platform_resources, checksum
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
backup.serviceId,
|
||||
backup.serviceName,
|
||||
backup.filename,
|
||||
backup.sizeBytes,
|
||||
backup.createdAt,
|
||||
backup.includesImage ? 1 : 0,
|
||||
JSON.stringify(backup.platformResources),
|
||||
backup.checksum,
|
||||
]
|
||||
);
|
||||
|
||||
// Get the created backup by looking for the most recent one with matching filename
|
||||
const rows = this.query(
|
||||
'SELECT * FROM backups WHERE filename = ? ORDER BY id DESC LIMIT 1',
|
||||
[backup.filename]
|
||||
);
|
||||
|
||||
return this.rowToBackup(rows[0]);
|
||||
}
|
||||
|
||||
getById(id: number): IBackup | null {
|
||||
const rows = this.query('SELECT * FROM backups WHERE id = ?', [id]);
|
||||
return rows.length > 0 ? this.rowToBackup(rows[0]) : null;
|
||||
}
|
||||
|
||||
getByService(serviceId: number): IBackup[] {
|
||||
const rows = this.query(
|
||||
'SELECT * FROM backups WHERE service_id = ? ORDER BY created_at DESC',
|
||||
[serviceId]
|
||||
);
|
||||
return rows.map((row) => this.rowToBackup(row));
|
||||
}
|
||||
|
||||
getAll(): IBackup[] {
|
||||
const rows = this.query('SELECT * FROM backups ORDER BY created_at DESC');
|
||||
return rows.map((row) => this.rowToBackup(row));
|
||||
}
|
||||
|
||||
delete(id: number): void {
|
||||
this.query('DELETE FROM backups WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
deleteByService(serviceId: number): void {
|
||||
this.query('DELETE FROM backups WHERE service_id = ?', [serviceId]);
|
||||
}
|
||||
|
||||
private rowToBackup(row: any): IBackup {
|
||||
let platformResources: TPlatformServiceType[] = [];
|
||||
const platformResourcesRaw = row.platform_resources;
|
||||
if (platformResourcesRaw) {
|
||||
try {
|
||||
platformResources = JSON.parse(String(platformResourcesRaw));
|
||||
} catch {
|
||||
platformResources = [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(row.id),
|
||||
serviceId: Number(row.service_id),
|
||||
serviceName: String(row.service_name),
|
||||
filename: String(row.filename),
|
||||
sizeBytes: Number(row.size_bytes),
|
||||
createdAt: Number(row.created_at),
|
||||
includesImage: Boolean(row.includes_image),
|
||||
platformResources,
|
||||
checksum: String(row.checksum),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,4 @@ export { CertificateRepository } from './certificate.repository.ts';
|
||||
export { AuthRepository } from './auth.repository.ts';
|
||||
export { MetricsRepository } from './metrics.repository.ts';
|
||||
export { PlatformRepository } from './platform.repository.ts';
|
||||
export { BackupRepository } from './backup.repository.ts';
|
||||
|
||||
@@ -119,6 +119,10 @@ export class ServiceRepository extends BaseRepository {
|
||||
fields.push('platform_requirements = ?');
|
||||
values.push(JSON.stringify(updates.platformRequirements));
|
||||
}
|
||||
if (updates.includeImageInBackup !== undefined) {
|
||||
fields.push('include_image_in_backup = ?');
|
||||
values.push(updates.includeImageInBackup ? 1 : 0);
|
||||
}
|
||||
|
||||
fields.push('updated_at = ?');
|
||||
values.push(Date.now());
|
||||
@@ -172,6 +176,9 @@ export class ServiceRepository extends BaseRepository {
|
||||
autoUpdateOnPush: row.auto_update_on_push ? Boolean(row.auto_update_on_push) : undefined,
|
||||
imageDigest: row.image_digest ? String(row.image_digest) : undefined,
|
||||
platformRequirements,
|
||||
includeImageInBackup: row.include_image_in_backup !== undefined
|
||||
? Boolean(row.include_image_in_backup)
|
||||
: true, // Default to true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user