f6a79c815e
- Add S3/MinIO storage adapter with multipart upload support - Implement database backup service for SQLite and PostgreSQL - Create backup manifest generator for tracking backup contents - Enhance backup service with S3 integration and incremental backups - Add restore service with safety measures and rollback capability - Create comprehensive test suite for all backup functionality - Add admin API endpoints for backup/restore management - Implement frontend UI with dashboard, configuration, and restore wizard - Add roadmap section to README with implemented backup feature This implementation provides: - Multiple backup destinations (local, rsync, S3/MinIO) - Intelligent change detection to minimize backup frequency - Full database backups with compression - Manifest-based restore with integrity validation - Pre-restore safety backups with rollback - Comprehensive error handling and monitoring - User-friendly admin interface 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
131 lines
6.5 KiB
JavaScript
131 lines
6.5 KiB
JavaScript
exports.up = function(knex) {
|
|
return knex.schema
|
|
// Add new settings to app_settings table
|
|
.table('app_settings', table => {
|
|
// S3 configuration enhancements
|
|
table.boolean('backup_s3_force_path_style').defaultTo(false).comment('Force path-style S3 URLs (for MinIO/self-hosted)');
|
|
table.boolean('backup_s3_ssl_enabled').defaultTo(true).comment('Enable SSL/TLS for S3 connections');
|
|
table.string('backup_s3_prefix', 255).comment('S3 key prefix for organizing backups');
|
|
|
|
// Backup features
|
|
table.boolean('backup_incremental').defaultTo(false).comment('Enable incremental backups');
|
|
table.boolean('backup_include_database').defaultTo(true).comment('Include database dumps in backups');
|
|
table.boolean('backup_manifest_enabled').defaultTo(true).comment('Generate backup manifests');
|
|
table.enum('backup_manifest_format', ['json', 'yaml']).defaultTo('json').comment('Manifest file format');
|
|
table.boolean('backup_encryption_enabled').defaultTo(false).comment('Enable backup encryption');
|
|
table.string('backup_database_schedule', 100).comment('Separate cron schedule for database-only backups');
|
|
})
|
|
|
|
// Enhance backup_runs table
|
|
.table('backup_runs', table => {
|
|
// Manifest tracking
|
|
table.string('manifest_path', 500).comment('Path to backup manifest file');
|
|
table.uuid('manifest_id').comment('Unique identifier for the manifest');
|
|
table.enum('manifest_format', ['json', 'yaml']).comment('Format of the manifest file');
|
|
|
|
// Incremental backup support
|
|
table.integer('parent_backup_id').unsigned().references('id').inTable('backup_runs').onDelete('SET NULL').comment('Parent backup for incremental backups');
|
|
table.enum('backup_mode', ['full', 'incremental', 'database']).defaultTo('full').comment('Type of backup performed');
|
|
|
|
// Add indexes for better query performance
|
|
table.index(['backup_mode', 'status'], 'idx_backup_runs_mode_status');
|
|
table.index(['parent_backup_id'], 'idx_backup_runs_parent');
|
|
table.index(['created_at', 'backup_mode'], 'idx_backup_runs_created_mode');
|
|
})
|
|
|
|
// Create backup_manifest table for storing detailed manifest metadata
|
|
.createTable('backup_manifest', table => {
|
|
table.increments('id').primary();
|
|
table.integer('backup_run_id').unsigned().notNullable().references('id').inTable('backup_runs').onDelete('CASCADE');
|
|
table.uuid('manifest_id').notNullable().unique().comment('Unique identifier matching backup_runs.manifest_id');
|
|
table.string('version', 20).notNullable().defaultTo('1.0.0').comment('Manifest schema version');
|
|
table.enum('format', ['json', 'yaml']).notNullable().defaultTo('json');
|
|
|
|
// Backup metadata
|
|
table.timestamp('backup_start').notNullable();
|
|
table.timestamp('backup_end').notNullable();
|
|
table.bigInteger('total_size').unsigned().comment('Total size of backup in bytes');
|
|
table.integer('file_count').unsigned().comment('Number of files in backup');
|
|
table.integer('photo_count').unsigned().comment('Number of photos backed up');
|
|
table.integer('event_count').unsigned().comment('Number of events backed up');
|
|
|
|
// Incremental backup metadata
|
|
table.boolean('is_incremental').defaultTo(false);
|
|
table.uuid('parent_manifest_id').comment('Parent manifest ID for incremental backups');
|
|
table.timestamp('incremental_since').comment('Timestamp for incremental backup baseline');
|
|
|
|
// Content checksums
|
|
table.string('checksum_algorithm', 50).defaultTo('sha256').comment('Algorithm used for checksums');
|
|
table.text('manifest_checksum').comment('Checksum of the manifest file itself');
|
|
|
|
// Storage information
|
|
table.string('storage_location', 500).comment('Primary storage location (local path or S3 URI)');
|
|
table.string('storage_provider', 50).comment('Storage provider (local, s3, etc.)');
|
|
|
|
// Encryption metadata
|
|
table.boolean('is_encrypted').defaultTo(false);
|
|
table.string('encryption_algorithm', 100).comment('Encryption algorithm used');
|
|
table.string('encryption_key_id', 255).comment('ID of encryption key used');
|
|
|
|
// Additional metadata as JSON
|
|
table.json('metadata').comment('Additional metadata as JSON');
|
|
|
|
// Timestamps
|
|
table.timestamps(true, true);
|
|
|
|
// Indexes
|
|
table.index(['backup_run_id'], 'idx_manifest_backup_run');
|
|
table.index(['manifest_id'], 'idx_manifest_uuid');
|
|
table.index(['parent_manifest_id'], 'idx_manifest_parent');
|
|
table.index(['backup_start', 'backup_end'], 'idx_manifest_time_range');
|
|
table.index(['is_incremental', 'created_at'], 'idx_manifest_incremental_created');
|
|
})
|
|
|
|
// Add composite indexes for common query patterns
|
|
.raw(`
|
|
CREATE INDEX IF NOT EXISTS idx_backup_runs_recent_successful
|
|
ON backup_runs(created_at DESC)
|
|
WHERE status = 'completed' AND backup_mode = 'full';
|
|
`)
|
|
.raw(`
|
|
CREATE INDEX IF NOT EXISTS idx_backup_runs_incremental_chain
|
|
ON backup_runs(parent_backup_id, created_at)
|
|
WHERE backup_mode = 'incremental';
|
|
`);
|
|
};
|
|
|
|
exports.down = function(knex) {
|
|
return knex.schema
|
|
// Drop indexes first
|
|
.raw('DROP INDEX IF EXISTS idx_backup_runs_incremental_chain;')
|
|
.raw('DROP INDEX IF EXISTS idx_backup_runs_recent_successful;')
|
|
|
|
// Drop backup_manifest table
|
|
.dropTableIfExists('backup_manifest')
|
|
|
|
// Remove columns from backup_runs table
|
|
.table('backup_runs', table => {
|
|
table.dropIndex(['backup_mode', 'status'], 'idx_backup_runs_mode_status');
|
|
table.dropIndex(['parent_backup_id'], 'idx_backup_runs_parent');
|
|
table.dropIndex(['created_at', 'backup_mode'], 'idx_backup_runs_created_mode');
|
|
|
|
table.dropColumn('manifest_path');
|
|
table.dropColumn('manifest_id');
|
|
table.dropColumn('manifest_format');
|
|
table.dropColumn('parent_backup_id');
|
|
table.dropColumn('backup_mode');
|
|
})
|
|
|
|
// Remove columns from app_settings table
|
|
.table('app_settings', table => {
|
|
table.dropColumn('backup_s3_force_path_style');
|
|
table.dropColumn('backup_s3_ssl_enabled');
|
|
table.dropColumn('backup_s3_prefix');
|
|
table.dropColumn('backup_incremental');
|
|
table.dropColumn('backup_include_database');
|
|
table.dropColumn('backup_manifest_enabled');
|
|
table.dropColumn('backup_manifest_format');
|
|
table.dropColumn('backup_encryption_enabled');
|
|
table.dropColumn('backup_database_schedule');
|
|
});
|
|
}; |