feat(backup): Add backup scheduling system with GFS retention, API and UI integration

This commit is contained in:
2025-11-27 21:42:07 +00:00
parent c5d239ab28
commit 6ba7e655e3
17 changed files with 2319 additions and 12 deletions

View File

@@ -8,7 +8,15 @@ import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
import type { Onebox } from './onebox.ts';
import type { IApiResponse, ICreateRegistryTokenRequest, IRegistryTokenView, TPlatformServiceType, IContainerStats } from '../types.ts';
import type {
IApiResponse,
ICreateRegistryTokenRequest,
IRegistryTokenView,
TPlatformServiceType,
IContainerStats,
IBackupScheduleCreate,
IBackupScheduleUpdate,
} from '../types.ts';
export class OneboxHttpServer {
private oneboxRef: Onebox;
@@ -343,6 +351,26 @@ export class OneboxHttpServer {
return await this.handleSetBackupPasswordRequest(req);
} else if (path === '/api/settings/backup-password' && method === 'GET') {
return await this.handleCheckBackupPasswordRequest();
// Backup Schedule endpoints
} else if (path === '/api/backup-schedules' && method === 'GET') {
return await this.handleListBackupSchedulesRequest();
} else if (path === '/api/backup-schedules' && method === 'POST') {
return await this.handleCreateBackupScheduleRequest(req);
} else if (path.match(/^\/api\/backup-schedules\/\d+$/) && method === 'GET') {
const scheduleId = Number(path.split('/').pop());
return await this.handleGetBackupScheduleRequest(scheduleId);
} else if (path.match(/^\/api\/backup-schedules\/\d+$/) && method === 'PUT') {
const scheduleId = Number(path.split('/').pop());
return await this.handleUpdateBackupScheduleRequest(scheduleId, req);
} else if (path.match(/^\/api\/backup-schedules\/\d+$/) && method === 'DELETE') {
const scheduleId = Number(path.split('/').pop());
return await this.handleDeleteBackupScheduleRequest(scheduleId);
} else if (path.match(/^\/api\/backup-schedules\/\d+\/trigger$/) && method === 'POST') {
const scheduleId = Number(path.split('/')[3]);
return await this.handleTriggerBackupScheduleRequest(scheduleId);
} else if (path.match(/^\/api\/services\/[^/]+\/backup-schedules$/) && method === 'GET') {
const serviceName = path.split('/')[3];
return await this.handleListServiceBackupSchedulesRequest(serviceName);
} else {
return this.jsonResponse({ success: false, error: 'Not found' }, 404);
}
@@ -2311,6 +2339,244 @@ export class OneboxHttpServer {
}
}
// ============ Backup Schedule Endpoints ============
/**
* List all backup schedules
*/
private async handleListBackupSchedulesRequest(): Promise<Response> {
try {
const schedules = this.oneboxRef.backupScheduler.getAllSchedules();
return this.jsonResponse({ success: true, data: schedules });
} catch (error) {
logger.error(`Failed to list backup schedules: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to list backup schedules',
}, 500);
}
}
/**
* Create a new backup schedule
*/
private async handleCreateBackupScheduleRequest(req: Request): Promise<Response> {
try {
const body = await req.json() as IBackupScheduleCreate;
// Validate scope type
if (!body.scopeType) {
return this.jsonResponse({
success: false,
error: 'Scope type is required (all, pattern, or service)',
}, 400);
}
if (!['all', 'pattern', 'service'].includes(body.scopeType)) {
return this.jsonResponse({
success: false,
error: 'Invalid scope type. Must be: all, pattern, or service',
}, 400);
}
// Validate scope-specific requirements
if (body.scopeType === 'service' && !body.serviceName) {
return this.jsonResponse({
success: false,
error: 'Service name is required for service-specific schedules',
}, 400);
}
if (body.scopeType === 'pattern' && !body.scopePattern) {
return this.jsonResponse({
success: false,
error: 'Scope pattern is required for pattern-based schedules',
}, 400);
}
if (!body.cronExpression) {
return this.jsonResponse({
success: false,
error: 'Cron expression is required',
}, 400);
}
if (!body.retention) {
return this.jsonResponse({
success: false,
error: 'Retention policy is required',
}, 400);
}
// Validate retention policy
const { hourly, daily, weekly, monthly } = body.retention;
if (typeof hourly !== 'number' || typeof daily !== 'number' ||
typeof weekly !== 'number' || typeof monthly !== 'number') {
return this.jsonResponse({
success: false,
error: 'Retention policy must have hourly, daily, weekly, and monthly as numbers',
}, 400);
}
if (hourly < 0 || daily < 0 || weekly < 0 || monthly < 0) {
return this.jsonResponse({
success: false,
error: 'Retention values must be non-negative',
}, 400);
}
const schedule = await this.oneboxRef.backupScheduler.createSchedule(body);
// Build descriptive message based on scope type
let scopeDesc: string;
switch (body.scopeType) {
case 'all':
scopeDesc = 'all services';
break;
case 'pattern':
scopeDesc = `pattern '${body.scopePattern}'`;
break;
case 'service':
scopeDesc = `service '${body.serviceName}'`;
break;
}
return this.jsonResponse({
success: true,
message: `Backup schedule created for ${scopeDesc}`,
data: schedule,
});
} catch (error) {
logger.error(`Failed to create backup schedule: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to create backup schedule',
}, 500);
}
}
/**
* Get a specific backup schedule
*/
private async handleGetBackupScheduleRequest(scheduleId: number): Promise<Response> {
try {
const schedule = this.oneboxRef.backupScheduler.getScheduleById(scheduleId);
if (!schedule) {
return this.jsonResponse({ success: false, error: 'Backup schedule not found' }, 404);
}
return this.jsonResponse({ success: true, data: schedule });
} catch (error) {
logger.error(`Failed to get backup schedule ${scheduleId}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to get backup schedule',
}, 500);
}
}
/**
* Update a backup schedule
*/
private async handleUpdateBackupScheduleRequest(scheduleId: number, req: Request): Promise<Response> {
try {
const body = await req.json() as IBackupScheduleUpdate;
// Validate retention policy if provided
if (body.retention) {
const { hourly, daily, weekly, monthly } = body.retention;
if (typeof hourly !== 'number' || typeof daily !== 'number' ||
typeof weekly !== 'number' || typeof monthly !== 'number') {
return this.jsonResponse({
success: false,
error: 'Retention policy must have hourly, daily, weekly, and monthly as numbers',
}, 400);
}
if (hourly < 0 || daily < 0 || weekly < 0 || monthly < 0) {
return this.jsonResponse({
success: false,
error: 'Retention values must be non-negative',
}, 400);
}
}
const schedule = await this.oneboxRef.backupScheduler.updateSchedule(scheduleId, body);
return this.jsonResponse({
success: true,
message: 'Backup schedule updated',
data: schedule,
});
} catch (error) {
logger.error(`Failed to update backup schedule ${scheduleId}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to update backup schedule',
}, 500);
}
}
/**
* Delete a backup schedule
*/
private async handleDeleteBackupScheduleRequest(scheduleId: number): Promise<Response> {
try {
await this.oneboxRef.backupScheduler.deleteSchedule(scheduleId);
return this.jsonResponse({
success: true,
message: 'Backup schedule deleted',
});
} catch (error) {
logger.error(`Failed to delete backup schedule ${scheduleId}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to delete backup schedule',
}, 500);
}
}
/**
* Trigger immediate backup for a schedule
*/
private async handleTriggerBackupScheduleRequest(scheduleId: number): Promise<Response> {
try {
await this.oneboxRef.backupScheduler.triggerBackup(scheduleId);
return this.jsonResponse({
success: true,
message: 'Backup triggered successfully',
});
} catch (error) {
logger.error(`Failed to trigger backup for schedule ${scheduleId}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to trigger backup',
}, 500);
}
}
/**
* List backup schedules for a specific service
*/
private async handleListServiceBackupSchedulesRequest(serviceName: string): Promise<Response> {
try {
const service = this.oneboxRef.services.getService(serviceName);
if (!service) {
return this.jsonResponse({ success: false, error: 'Service not found' }, 404);
}
const schedules = this.oneboxRef.backupScheduler.getSchedulesForService(serviceName);
return this.jsonResponse({ success: true, data: schedules });
} catch (error) {
logger.error(`Failed to list backup schedules for service ${serviceName}: ${getErrorMessage(error)}`);
return this.jsonResponse({
success: false,
error: getErrorMessage(error) || 'Failed to list backup schedules',
}, 500);
}
}
/**
* Helper to create JSON response
*/