feat(backup): Add backup scheduling system with GFS retention, API and UI integration
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user