feat: implement comprehensive backup and restore system with S3 support
Mirror to GitHub / mirror (push) Successful in 28s
Test and Lint / backend-test (push) Successful in 1m26s
continuous-integration/drone/push Build is failing
Test and Lint / frontend-test (push) Failing after 2m18s
Version and Release / version-bump (push) Successful in 42s
Version and Release / trigger-drone (push) Successful in 3s
Mirror to GitHub / mirror (push) Successful in 28s
Test and Lint / backend-test (push) Successful in 1m26s
continuous-integration/drone/push Build is failing
Test and Lint / frontend-test (push) Failing after 2m18s
Version and Release / version-bump (push) Successful in 42s
Version and Release / trigger-drone (push) Successful in 3s
- 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>
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const { databaseBackupService } = require('../services/databaseBackup');
|
||||
const { db } = require('../database/db');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// All routes require admin authentication
|
||||
router.use(adminAuth);
|
||||
|
||||
/**
|
||||
* Get database backup status and configuration
|
||||
*/
|
||||
router.get('/status', async (req, res) => {
|
||||
try {
|
||||
// Get configuration
|
||||
const config = await databaseBackupService.getBackupConfig();
|
||||
|
||||
// Get recent backup history
|
||||
const history = await databaseBackupService.getBackupHistory(10);
|
||||
|
||||
// Get current progress if running
|
||||
const progress = databaseBackupService.getProgress();
|
||||
|
||||
// Calculate health status
|
||||
const lastBackup = history[0];
|
||||
const isHealthy = lastBackup && lastBackup.status === 'completed' &&
|
||||
new Date(lastBackup.completed_at) > new Date(Date.now() - 48 * 60 * 60 * 1000); // Within 48 hours
|
||||
|
||||
res.json({
|
||||
config,
|
||||
isRunning: databaseBackupService.isRunning,
|
||||
isHealthy,
|
||||
currentProgress: progress,
|
||||
lastBackup,
|
||||
recentBackups: history,
|
||||
dbType: databaseBackupService.dbType
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get database backup status:', error);
|
||||
res.status(500).json({ error: 'Failed to get backup status' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update database backup configuration
|
||||
*/
|
||||
router.put('/config', async (req, res) => {
|
||||
try {
|
||||
const allowedSettings = [
|
||||
'database_backup_enabled',
|
||||
'database_backup_schedule',
|
||||
'database_backup_destination_path',
|
||||
'database_backup_compress',
|
||||
'database_backup_validate_integrity',
|
||||
'database_backup_include_checksums',
|
||||
'database_backup_retention_days',
|
||||
'database_backup_email_on_failure',
|
||||
'database_backup_email_on_success'
|
||||
];
|
||||
|
||||
const updates = [];
|
||||
|
||||
for (const [key, value] of Object.entries(req.body)) {
|
||||
if (allowedSettings.includes(key)) {
|
||||
// Check if setting exists
|
||||
const existing = await db('app_settings')
|
||||
.where('setting_key', key)
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
await db('app_settings')
|
||||
.where('setting_key', key)
|
||||
.update({
|
||||
setting_value: JSON.stringify(value),
|
||||
updated_at: new Date()
|
||||
});
|
||||
} else {
|
||||
await db('app_settings').insert({
|
||||
setting_key: key,
|
||||
setting_value: JSON.stringify(value),
|
||||
setting_type: 'database_backup'
|
||||
});
|
||||
}
|
||||
|
||||
updates.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Restart scheduled backups if enabled state changed
|
||||
if (updates.includes('database_backup_enabled') || updates.includes('database_backup_schedule')) {
|
||||
const { startScheduledBackups, stopScheduledBackups } = require('../services/databaseBackup');
|
||||
stopScheduledBackups();
|
||||
await startScheduledBackups();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
updatedSettings: updates,
|
||||
message: 'Database backup configuration updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to update database backup config:', error);
|
||||
res.status(500).json({ error: 'Failed to update configuration' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Trigger manual database backup
|
||||
*/
|
||||
router.post('/backup', async (req, res) => {
|
||||
try {
|
||||
if (databaseBackupService.isRunning) {
|
||||
return res.status(409).json({ error: 'Backup already in progress' });
|
||||
}
|
||||
|
||||
// Start backup asynchronously
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Database backup started',
|
||||
trackingUrl: '/api/admin/database-backup/progress'
|
||||
});
|
||||
|
||||
// Run backup in background
|
||||
databaseBackupService.backup(req.body).catch(error => {
|
||||
logger.error('Manual database backup failed:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start database backup:', error);
|
||||
res.status(500).json({ error: 'Failed to start backup' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get current backup progress
|
||||
*/
|
||||
router.get('/progress', async (req, res) => {
|
||||
try {
|
||||
const progress = databaseBackupService.getProgress();
|
||||
|
||||
res.json({
|
||||
isRunning: databaseBackupService.isRunning,
|
||||
progress
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get backup progress:', error);
|
||||
res.status(500).json({ error: 'Failed to get progress' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get backup history with pagination
|
||||
*/
|
||||
router.get('/history', async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const [backups, totalCount] = await Promise.all([
|
||||
db('database_backup_runs')
|
||||
.orderBy('started_at', 'desc')
|
||||
.limit(limit)
|
||||
.offset(offset),
|
||||
db('database_backup_runs').count('* as count').first()
|
||||
]);
|
||||
|
||||
res.json({
|
||||
backups,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: totalCount.count,
|
||||
pages: Math.ceil(totalCount.count / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get backup history:', error);
|
||||
res.status(500).json({ error: 'Failed to get history' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete old backup files
|
||||
*/
|
||||
router.delete('/cleanup', async (req, res) => {
|
||||
try {
|
||||
const { retentionDays = 30 } = req.body;
|
||||
|
||||
await databaseBackupService.cleanupOldBackups(retentionDays);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Cleaned up backups older than ${retentionDays} days`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to cleanup old backups:', error);
|
||||
res.status(500).json({ error: 'Failed to cleanup backups' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Test database backup configuration
|
||||
*/
|
||||
router.post('/test', async (req, res) => {
|
||||
try {
|
||||
const config = await databaseBackupService.getBackupConfig();
|
||||
|
||||
// Test database connection
|
||||
const testResults = {
|
||||
databaseConnection: false,
|
||||
destinationWritable: false,
|
||||
compressionAvailable: true,
|
||||
estimatedSize: null
|
||||
};
|
||||
|
||||
// Test database connection
|
||||
try {
|
||||
await db.raw('SELECT 1');
|
||||
testResults.databaseConnection = true;
|
||||
} catch (error) {
|
||||
testResults.databaseConnectionError = error.message;
|
||||
}
|
||||
|
||||
// Test destination path
|
||||
if (config.destinationPath) {
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
const testFile = `${config.destinationPath}/.test-${Date.now()}`;
|
||||
await fs.writeFile(testFile, 'test');
|
||||
await fs.unlink(testFile);
|
||||
testResults.destinationWritable = true;
|
||||
} catch (error) {
|
||||
testResults.destinationError = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Estimate database size
|
||||
try {
|
||||
testResults.estimatedSize = await databaseBackupService.getDatabaseSize();
|
||||
} catch (error) {
|
||||
testResults.sizeError = error.message;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: testResults.databaseConnection && testResults.destinationWritable,
|
||||
results: testResults
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to test backup configuration:', error);
|
||||
res.status(500).json({ error: 'Failed to test configuration' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get table checksums
|
||||
*/
|
||||
router.get('/checksums', async (req, res) => {
|
||||
try {
|
||||
const checksums = await databaseBackupService.getTableChecksums();
|
||||
|
||||
res.json({
|
||||
checksums,
|
||||
tableCount: Object.keys(checksums).length,
|
||||
totalRows: Object.values(checksums).reduce((sum, table) => sum + table.rowCount, 0)
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get table checksums:', error);
|
||||
res.status(500).json({ error: 'Failed to get checksums' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user