Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c210507951 | |||
| 0799efadae |
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-24 - 1.24.0 - feat(backup)
|
||||||
|
add containerarchive-backed backup storage, restore, download, and pruning support
|
||||||
|
|
||||||
|
- add database support for archive snapshot IDs and stored size tracking for backups
|
||||||
|
- initialize and close the backup archive during onebox lifecycle startup and shutdown
|
||||||
|
- allow backup download and restore flows to work with archive snapshots as well as legacy file-based backups
|
||||||
|
- schedule daily archive pruning based on the most generous configured retention policy
|
||||||
|
- replace smarts3 with smartstorage for registry-backed S3-compatible storage
|
||||||
|
|
||||||
## 2026-03-21 - 1.23.0 - feat(appstore)
|
## 2026-03-21 - 1.23.0 - feat(appstore)
|
||||||
add remote app store templates with service upgrades and Redis/MariaDB platform support
|
add remote app store templates with service upgrades and Redis/MariaDB platform support
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/onebox",
|
"name": "@serve.zone/onebox",
|
||||||
"version": "1.23.0",
|
"version": "1.24.0",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"test": "deno test --allow-all test/",
|
"test": "deno test --allow-all test/",
|
||||||
@@ -19,14 +19,15 @@
|
|||||||
"@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@6.4.3",
|
"@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@6.4.3",
|
||||||
"@push.rocks/smartacme": "npm:@push.rocks/smartacme@^8.0.0",
|
"@push.rocks/smartacme": "npm:@push.rocks/smartacme@^8.0.0",
|
||||||
"@push.rocks/smartregistry": "npm:@push.rocks/smartregistry@^2.2.0",
|
"@push.rocks/smartregistry": "npm:@push.rocks/smartregistry@^2.2.0",
|
||||||
"@push.rocks/smarts3": "npm:@push.rocks/smarts3@^5.1.0",
|
"@push.rocks/smartstorage": "npm:@push.rocks/smartstorage@^6.3.0",
|
||||||
"@push.rocks/taskbuffer": "npm:@push.rocks/taskbuffer@^3.1.0",
|
"@push.rocks/taskbuffer": "npm:@push.rocks/taskbuffer@^3.1.0",
|
||||||
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
|
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
|
||||||
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
|
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
|
||||||
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1",
|
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1",
|
||||||
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
|
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
|
||||||
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
|
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
|
||||||
"@api.global/typedsocket": "npm:@api.global/typedsocket@^4.1.2"
|
"@api.global/typedsocket": "npm:@api.global/typedsocket@^4.1.2",
|
||||||
|
"@serve.zone/containerarchive": "npm:@serve.zone/containerarchive@^0.1.3"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/onebox",
|
"name": "@serve.zone/onebox",
|
||||||
"version": "1.23.0",
|
"version": "1.24.0",
|
||||||
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
|
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
|
||||||
"main": "mod.ts",
|
"main": "mod.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/onebox',
|
name: '@serve.zone/onebox',
|
||||||
version: '1.23.0',
|
version: '1.24.0',
|
||||||
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,15 @@ export class BackupScheduler {
|
|||||||
await this.registerTask(schedule);
|
await this.registerTask(schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add periodic archive prune task (runs daily at 3 AM)
|
||||||
|
const pruneTask = new plugins.taskbuffer.Task({
|
||||||
|
name: 'backup-archive-prune',
|
||||||
|
taskFunction: async () => {
|
||||||
|
await this.pruneArchive();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.taskManager.addAndScheduleTask(pruneTask, '0 3 * * *');
|
||||||
|
|
||||||
// Start the task manager (activates cron scheduling)
|
// Start the task manager (activates cron scheduling)
|
||||||
await this.taskManager.start();
|
await this.taskManager.start();
|
||||||
|
|
||||||
@@ -436,9 +445,11 @@ export class BackupScheduler {
|
|||||||
if (!toKeep.has(backup.id!)) {
|
if (!toKeep.has(backup.id!)) {
|
||||||
try {
|
try {
|
||||||
await this.oneboxRef.backupManager.deleteBackup(backup.id!);
|
await this.oneboxRef.backupManager.deleteBackup(backup.id!);
|
||||||
logger.info(`Deleted backup ${backup.filename} (retention policy)`);
|
const backupRef = backup.snapshotId || backup.filename;
|
||||||
|
logger.info(`Deleted backup ${backupRef} (retention policy)`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Failed to delete old backup ${backup.filename}: ${getErrorMessage(error)}`);
|
const backupRef = backup.snapshotId || backup.filename;
|
||||||
|
logger.warn(`Failed to delete old backup ${backupRef}: ${getErrorMessage(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -647,4 +658,48 @@ export class BackupScheduler {
|
|||||||
private getRetentionDescription(retention: IRetentionPolicy): string {
|
private getRetentionDescription(retention: IRetentionPolicy): string {
|
||||||
return `H:${retention.hourly} D:${retention.daily} W:${retention.weekly} M:${retention.monthly}`;
|
return `H:${retention.hourly} D:${retention.daily} W:${retention.weekly} M:${retention.monthly}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune the containerarchive repository to reclaim storage.
|
||||||
|
* Uses the most generous retention policy across all schedules.
|
||||||
|
*/
|
||||||
|
private async pruneArchive(): Promise<void> {
|
||||||
|
const archive = this.oneboxRef.backupManager.archive;
|
||||||
|
if (!archive) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Compute the most generous retention across all schedules
|
||||||
|
const schedules = this.oneboxRef.database.getAllBackupSchedules();
|
||||||
|
|
||||||
|
// Default minimums if no schedules exist
|
||||||
|
let maxDays = 7;
|
||||||
|
let maxWeeks = 4;
|
||||||
|
let maxMonths = 12;
|
||||||
|
|
||||||
|
for (const schedule of schedules) {
|
||||||
|
if (schedule.retention.daily > maxDays) maxDays = schedule.retention.daily;
|
||||||
|
if (schedule.retention.weekly > maxWeeks) maxWeeks = schedule.retention.weekly;
|
||||||
|
if (schedule.retention.monthly > maxMonths) maxMonths = schedule.retention.monthly;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await archive.prune(
|
||||||
|
{
|
||||||
|
keepDays: maxDays,
|
||||||
|
keepWeeks: maxWeeks,
|
||||||
|
keepMonths: maxMonths,
|
||||||
|
},
|
||||||
|
false, // not dry run
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.removedSnapshots > 0 || result.freedBytes > 0) {
|
||||||
|
const freedMB = Math.round(result.freedBytes / (1024 * 1024) * 10) / 10;
|
||||||
|
logger.info(
|
||||||
|
`Archive prune: removed ${result.removedSnapshots} snapshot(s), ` +
|
||||||
|
`${result.removedPacks} pack(s), freed ${freedMB} MB`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Archive prune failed: ${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2161,27 +2161,47 @@ export class OneboxHttpServer {
|
|||||||
*/
|
*/
|
||||||
private async handleDownloadBackupRequest(backupId: number): Promise<Response> {
|
private async handleDownloadBackupRequest(backupId: number): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const filePath = this.oneboxRef.backupManager.getBackupFilePath(backupId);
|
const backup = this.oneboxRef.database.getBackupById(backupId);
|
||||||
if (!filePath) {
|
if (!backup) {
|
||||||
return this.jsonResponse({ success: false, error: 'Backup not found' }, 404);
|
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
|
// Check if file exists
|
||||||
try {
|
try {
|
||||||
await Deno.stat(filePath);
|
await Deno.stat(downloadPath);
|
||||||
} catch {
|
} catch {
|
||||||
return this.jsonResponse({ success: false, error: 'Backup file not found on disk' }, 404);
|
return this.jsonResponse({ success: false, error: 'Backup file not found on disk' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read file and return as download
|
const file = await Deno.readFile(downloadPath);
|
||||||
const backup = this.oneboxRef.database.getBackupById(backupId);
|
const filename = backup.filename || `${backup.serviceName}-${backup.createdAt}.tar.enc`;
|
||||||
const file = await Deno.readFile(filePath);
|
|
||||||
|
// Clean up temp export file
|
||||||
|
if (tempExport) {
|
||||||
|
try { await Deno.remove(downloadPath); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(file, {
|
return new Response(file, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/octet-stream',
|
'Content-Type': 'application/octet-stream',
|
||||||
'Content-Disposition': `attachment; filename="${backup?.filename || 'backup.tar.enc'}"`,
|
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||||
'Content-Length': String(file.length),
|
'Content-Length': String(file.length),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -2241,12 +2261,6 @@ export class OneboxHttpServer {
|
|||||||
}, 400);
|
}, 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
|
// Validate mode-specific requirements
|
||||||
if ((mode === 'import' || mode === 'clone') && !newServiceName) {
|
if ((mode === 'import' || mode === 'clone') && !newServiceName) {
|
||||||
return this.jsonResponse({
|
return this.jsonResponse({
|
||||||
@@ -2255,7 +2269,7 @@ export class OneboxHttpServer {
|
|||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.oneboxRef.backupManager.restoreBackup(filePath, {
|
const result = await this.oneboxRef.backupManager.restoreBackup(backupId, {
|
||||||
mode,
|
mode,
|
||||||
newServiceName,
|
newServiceName,
|
||||||
overwriteExisting: overwriteExisting === true,
|
overwriteExisting: overwriteExisting === true,
|
||||||
|
|||||||
@@ -192,6 +192,14 @@ export class Onebox {
|
|||||||
// Start auto-update monitoring for registry services
|
// Start auto-update monitoring for registry services
|
||||||
this.services.startAutoUpdateMonitoring();
|
this.services.startAutoUpdateMonitoring();
|
||||||
|
|
||||||
|
// Initialize BackupManager (containerarchive repository, non-critical)
|
||||||
|
try {
|
||||||
|
await this.backupManager.init();
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('BackupManager initialization failed - backups will be limited');
|
||||||
|
logger.warn(`Error: ${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize Backup Scheduler (non-critical)
|
// Initialize Backup Scheduler (non-critical)
|
||||||
try {
|
try {
|
||||||
await this.backupScheduler.init();
|
await this.backupScheduler.init();
|
||||||
@@ -430,6 +438,9 @@ export class Onebox {
|
|||||||
// Stop Caddy log receiver
|
// Stop Caddy log receiver
|
||||||
await this.caddyLogReceiver.stop();
|
await this.caddyLogReceiver.stop();
|
||||||
|
|
||||||
|
// Close backup archive
|
||||||
|
await this.backupManager.close();
|
||||||
|
|
||||||
// Close database
|
// Close database
|
||||||
this.database.close();
|
this.database.close();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Onebox Registry Manager
|
* Onebox Registry Manager
|
||||||
*
|
*
|
||||||
* Manages the local Docker registry using:
|
* Manages the local Docker registry using:
|
||||||
* - @push.rocks/smarts3 (S3-compatible server with filesystem storage)
|
* - @push.rocks/smartstorage (S3-compatible server with filesystem storage)
|
||||||
* - @push.rocks/smartregistry (OCI-compliant Docker registry)
|
* - @push.rocks/smartregistry (OCI-compliant Docker registry)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export class RegistryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the registry (start smarts3 and smartregistry)
|
* Initialize the registry (start smartstorage and smartregistry)
|
||||||
*/
|
*/
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.isInitialized) {
|
if (this.isInitialized) {
|
||||||
@@ -39,10 +39,10 @@ export class RegistryManager {
|
|||||||
const dataDir = this.options.dataDir || './.nogit/registry-data';
|
const dataDir = this.options.dataDir || './.nogit/registry-data';
|
||||||
const port = this.options.port || 4000;
|
const port = this.options.port || 4000;
|
||||||
|
|
||||||
logger.info(`Starting smarts3 server on port ${port}...`);
|
logger.info(`Starting smartstorage server on port ${port}...`);
|
||||||
|
|
||||||
// 1. Start smarts3 server (S3-compatible storage with filesystem backend)
|
// 1. Start smartstorage server (S3-compatible storage with filesystem backend)
|
||||||
this.s3Server = await plugins.smarts3.Smarts3.createAndStart({
|
this.s3Server = await plugins.smartstorage.SmartStorage.createAndStart({
|
||||||
server: {
|
server: {
|
||||||
port: port,
|
port: port,
|
||||||
address: '0.0.0.0',
|
address: '0.0.0.0',
|
||||||
@@ -53,16 +53,16 @@ export class RegistryManager {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.success(`smarts3 server started on port ${port}`);
|
logger.success(`smartstorage server started on port ${port}`);
|
||||||
|
|
||||||
// 2. Configure smartregistry to use smarts3
|
// 2. Configure smartregistry to use smartstorage
|
||||||
logger.info('Initializing smartregistry...');
|
logger.info('Initializing smartregistry...');
|
||||||
|
|
||||||
this.registry = new plugins.smartregistry.SmartRegistry({
|
this.registry = new plugins.smartregistry.SmartRegistry({
|
||||||
storage: {
|
storage: {
|
||||||
endpoint: 'localhost',
|
endpoint: 'localhost',
|
||||||
port: port,
|
port: port,
|
||||||
accessKey: 'onebox', // smarts3 doesn't validate credentials
|
accessKey: 'onebox', // smartstorage doesn't validate credentials
|
||||||
accessSecret: 'onebox',
|
accessSecret: 'onebox',
|
||||||
useSsl: false,
|
useSsl: false,
|
||||||
region: 'us-east-1',
|
region: 'us-east-1',
|
||||||
@@ -314,15 +314,15 @@ export class RegistryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the registry and smarts3 server
|
* Stop the registry and smartstorage server
|
||||||
*/
|
*/
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
if (this.s3Server) {
|
if (this.s3Server) {
|
||||||
try {
|
try {
|
||||||
await this.s3Server.stop();
|
await this.s3Server.stop();
|
||||||
logger.info('smarts3 server stopped');
|
logger.info('smartstorage server stopped');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error stopping smarts3: ${getErrorMessage(error)}`);
|
logger.error(`Error stopping smartstorage: ${getErrorMessage(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -607,6 +607,10 @@ export class OneboxDatabase {
|
|||||||
return this.backupRepo.getBySchedule(scheduleId);
|
return this.backupRepo.getBySchedule(scheduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBackupBySnapshotId(snapshotId: string): IBackup | null {
|
||||||
|
return this.backupRepo.getBySnapshotId(snapshotId);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Backup Schedules (delegated to repository) ============
|
// ============ Backup Schedules (delegated to repository) ============
|
||||||
|
|
||||||
createBackupSchedule(schedule: Omit<IBackupSchedule, 'id'>): IBackupSchedule {
|
createBackupSchedule(schedule: Omit<IBackupSchedule, 'id'>): IBackupSchedule {
|
||||||
|
|||||||
13
ts/database/migrations/migration-014-containerarchive.ts
Normal file
13
ts/database/migrations/migration-014-containerarchive.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { BaseMigration } from './base-migration.ts';
|
||||||
|
import type { TQueryFunction } from '../types.ts';
|
||||||
|
|
||||||
|
export class Migration014ContainerArchive extends BaseMigration {
|
||||||
|
readonly version = 14;
|
||||||
|
readonly description = 'Add containerarchive snapshot tracking to backups';
|
||||||
|
|
||||||
|
up(query: TQueryFunction): void {
|
||||||
|
query('ALTER TABLE backups ADD COLUMN snapshot_id TEXT');
|
||||||
|
query('ALTER TABLE backups ADD COLUMN stored_size_bytes INTEGER DEFAULT 0');
|
||||||
|
query('CREATE INDEX IF NOT EXISTS idx_backups_snapshot ON backups(snapshot_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import { Migration010BackupSchedules } from './migration-010-backup-schedules.ts
|
|||||||
import { Migration011ScopeColumns } from './migration-011-scope-columns.ts';
|
import { Migration011ScopeColumns } from './migration-011-scope-columns.ts';
|
||||||
import { Migration012GfsRetention } from './migration-012-gfs-retention.ts';
|
import { Migration012GfsRetention } from './migration-012-gfs-retention.ts';
|
||||||
import { Migration013AppTemplateVersion } from './migration-013-app-template-version.ts';
|
import { Migration013AppTemplateVersion } from './migration-013-app-template-version.ts';
|
||||||
|
import { Migration014ContainerArchive } from './migration-014-containerarchive.ts';
|
||||||
import type { BaseMigration } from './base-migration.ts';
|
import type { BaseMigration } from './base-migration.ts';
|
||||||
|
|
||||||
export class MigrationRunner {
|
export class MigrationRunner {
|
||||||
@@ -44,6 +45,7 @@ export class MigrationRunner {
|
|||||||
new Migration011ScopeColumns(),
|
new Migration011ScopeColumns(),
|
||||||
new Migration012GfsRetention(),
|
new Migration012GfsRetention(),
|
||||||
new Migration013AppTemplateVersion(),
|
new Migration013AppTemplateVersion(),
|
||||||
|
new Migration014ContainerArchive(),
|
||||||
].sort((a, b) => a.version - b.version);
|
].sort((a, b) => a.version - b.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ export class BackupRepository extends BaseRepository {
|
|||||||
this.query(
|
this.query(
|
||||||
`INSERT INTO backups (
|
`INSERT INTO backups (
|
||||||
service_id, service_name, filename, size_bytes, created_at,
|
service_id, service_name, filename, size_bytes, created_at,
|
||||||
includes_image, platform_resources, checksum, schedule_id
|
includes_image, platform_resources, checksum, schedule_id,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
snapshot_id, stored_size_bytes
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
backup.serviceId,
|
backup.serviceId,
|
||||||
backup.serviceName,
|
backup.serviceName,
|
||||||
@@ -32,6 +33,8 @@ export class BackupRepository extends BaseRepository {
|
|||||||
JSON.stringify(backup.platformResources),
|
JSON.stringify(backup.platformResources),
|
||||||
backup.checksum,
|
backup.checksum,
|
||||||
backup.scheduleId ?? null,
|
backup.scheduleId ?? null,
|
||||||
|
backup.snapshotId ?? null,
|
||||||
|
backup.storedSizeBytes ?? 0,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -78,6 +81,14 @@ export class BackupRepository extends BaseRepository {
|
|||||||
return rows.map((row) => this.rowToBackup(row));
|
return rows.map((row) => this.rowToBackup(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBySnapshotId(snapshotId: string): IBackup | null {
|
||||||
|
const rows = this.query(
|
||||||
|
'SELECT * FROM backups WHERE snapshot_id = ?',
|
||||||
|
[snapshotId]
|
||||||
|
);
|
||||||
|
return rows.length > 0 ? this.rowToBackup(rows[0]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
private rowToBackup(row: any): IBackup {
|
private rowToBackup(row: any): IBackup {
|
||||||
let platformResources: TPlatformServiceType[] = [];
|
let platformResources: TPlatformServiceType[] = [];
|
||||||
const platformResourcesRaw = row.platform_resources;
|
const platformResourcesRaw = row.platform_resources;
|
||||||
@@ -94,7 +105,9 @@ export class BackupRepository extends BaseRepository {
|
|||||||
serviceId: Number(row.service_id),
|
serviceId: Number(row.service_id),
|
||||||
serviceName: String(row.service_name),
|
serviceName: String(row.service_name),
|
||||||
filename: String(row.filename),
|
filename: String(row.filename),
|
||||||
|
snapshotId: row.snapshot_id ? String(row.snapshot_id) : undefined,
|
||||||
sizeBytes: Number(row.size_bytes),
|
sizeBytes: Number(row.size_bytes),
|
||||||
|
storedSizeBytes: row.stored_size_bytes ? Number(row.stored_size_bytes) : undefined,
|
||||||
createdAt: Number(row.created_at),
|
createdAt: Number(row.created_at),
|
||||||
includesImage: Boolean(row.includes_image),
|
includesImage: Boolean(row.includes_image),
|
||||||
platformResources,
|
platformResources,
|
||||||
|
|||||||
@@ -53,12 +53,8 @@ export class BackupsHandler {
|
|||||||
'restoreBackup',
|
'restoreBackup',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
|
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||||
const backupPath = this.opsServerRef.oneboxRef.backupManager.getBackupFilePath(dataArg.backupId);
|
|
||||||
if (!backupPath) {
|
|
||||||
throw new plugins.typedrequest.TypedResponseError('Backup file not found');
|
|
||||||
}
|
|
||||||
const rawResult = await this.opsServerRef.oneboxRef.backupManager.restoreBackup(
|
const rawResult = await this.opsServerRef.oneboxRef.backupManager.restoreBackup(
|
||||||
backupPath,
|
dataArg.backupId,
|
||||||
dataArg.options,
|
dataArg.options,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -84,14 +80,11 @@ export class BackupsHandler {
|
|||||||
if (!backup) {
|
if (!backup) {
|
||||||
throw new plugins.typedrequest.TypedResponseError('Backup not found');
|
throw new plugins.typedrequest.TypedResponseError('Backup not found');
|
||||||
}
|
}
|
||||||
const filePath = this.opsServerRef.oneboxRef.backupManager.getBackupFilePath(dataArg.backupId);
|
|
||||||
if (!filePath) {
|
|
||||||
throw new plugins.typedrequest.TypedResponseError('Backup file not found');
|
|
||||||
}
|
|
||||||
// Return a download URL that the client can fetch directly
|
// Return a download URL that the client can fetch directly
|
||||||
|
const filename = backup.filename || `${backup.serviceName}-${backup.createdAt}.tar.enc`;
|
||||||
return {
|
return {
|
||||||
downloadUrl: `/api/backups/${dataArg.backupId}/download`,
|
downloadUrl: `/api/backups/${dataArg.backupId}/download`,
|
||||||
filename: backup.filename,
|
filename,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import * as smartregistry from '@push.rocks/smartregistry';
|
|||||||
export { smartregistry };
|
export { smartregistry };
|
||||||
|
|
||||||
// S3-compatible storage server
|
// S3-compatible storage server
|
||||||
import * as smarts3 from '@push.rocks/smarts3';
|
import * as smartstorage from '@push.rocks/smartstorage';
|
||||||
export { smarts3 };
|
export { smartstorage };
|
||||||
|
|
||||||
// Task scheduling and cron jobs
|
// Task scheduling and cron jobs
|
||||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
@@ -67,3 +67,12 @@ export { typedrequest, typedserver };
|
|||||||
import * as smartguard from '@push.rocks/smartguard';
|
import * as smartguard from '@push.rocks/smartguard';
|
||||||
import * as smartjwt from '@push.rocks/smartjwt';
|
import * as smartjwt from '@push.rocks/smartjwt';
|
||||||
export { smartguard, smartjwt };
|
export { smartguard, smartjwt };
|
||||||
|
|
||||||
|
// Backup archive (content-addressed dedup storage)
|
||||||
|
import { ContainerArchive } from '@serve.zone/containerarchive';
|
||||||
|
export { ContainerArchive };
|
||||||
|
|
||||||
|
// Node.js compat for streaming
|
||||||
|
import * as nodeFs from 'node:fs';
|
||||||
|
import * as nodeStream from 'node:stream';
|
||||||
|
export { nodeFs, nodeStream };
|
||||||
|
|||||||
@@ -356,7 +356,9 @@ export interface IBackup {
|
|||||||
serviceId: number;
|
serviceId: number;
|
||||||
serviceName: string; // Denormalized for display
|
serviceName: string; // Denormalized for display
|
||||||
filename: string;
|
filename: string;
|
||||||
|
snapshotId?: string; // ContainerArchive snapshot ID (new backups)
|
||||||
sizeBytes: number;
|
sizeBytes: number;
|
||||||
|
storedSizeBytes?: number; // Actual stored size after dedup+compression
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
includesImage: boolean;
|
includesImage: boolean;
|
||||||
platformResources: TPlatformServiceType[]; // Which platform types were backed up
|
platformResources: TPlatformServiceType[]; // Which platform types were backed up
|
||||||
@@ -399,7 +401,8 @@ export interface IBackupPlatformResource {
|
|||||||
|
|
||||||
export interface IBackupResult {
|
export interface IBackupResult {
|
||||||
backup: IBackup;
|
backup: IBackup;
|
||||||
filePath: string;
|
filePath?: string; // Legacy file-based backups only
|
||||||
|
snapshotId?: string; // ContainerArchive snapshot ID
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRestoreOptions {
|
export interface IRestoreOptions {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -28,7 +28,9 @@ export interface IBackup {
|
|||||||
serviceId: number;
|
serviceId: number;
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
snapshotId?: string;
|
||||||
sizeBytes: number;
|
sizeBytes: number;
|
||||||
|
storedSizeBytes?: number;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
includesImage: boolean;
|
includesImage: boolean;
|
||||||
platformResources: TPlatformServiceType[];
|
platformResources: TPlatformServiceType[];
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/onebox',
|
name: '@serve.zone/onebox',
|
||||||
version: '1.23.0',
|
version: '1.24.0',
|
||||||
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user