382 lines
12 KiB
JavaScript
Executable File
382 lines
12 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
/**
|
||
* SQLite Backup CLI Tool
|
||
*
|
||
* Command-line interface for the SQLite Backup Library
|
||
*/
|
||
|
||
const { SQLiteBackup, BackupUtils } = require('../lib/index.js');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
|
||
function showHelp() {
|
||
console.log(`
|
||
SQLite Backup CLI Tool
|
||
|
||
Usage: sqlite-backup <command> [options]
|
||
|
||
Commands:
|
||
create <database> Create a backup of the specified database
|
||
list <database> List all backups for the specified database
|
||
cleanup <database> Clean up old backups
|
||
restore <backup> <database> Restore a backup to a database
|
||
verify <backup> Verify backup integrity
|
||
help Show this help message
|
||
|
||
Options:
|
||
--backup-dir <dir> Directory to store backups (default: <database-dir>/backups)
|
||
--filename <name> Custom filename for backup
|
||
--no-timestamp Don't include timestamp in filename
|
||
--no-verify Skip backup verification
|
||
--method <method> Backup method: backup, copy, vacuum (default: backup)
|
||
--retention-days <days> Number of days to keep backups for cleanup
|
||
--max-backups <number> Maximum number of backups to keep
|
||
--target <path> Target path for restore
|
||
--include-checksums Include checksums when listing backups
|
||
--verbose Enable verbose output
|
||
|
||
Examples:
|
||
sqlite-backup create ./data/app.db
|
||
sqlite-backup create ./data/app.db --backup-dir ./backups --filename custom-backup
|
||
sqlite-backup list ./data/app.db --include-checksums
|
||
sqlite-backup cleanup ./data/app.db --retention-days 30
|
||
sqlite-backup restore ./backups/backup.db ./data/app.db
|
||
sqlite-backup verify ./backups/backup.db
|
||
`);
|
||
}
|
||
|
||
function parseArgs() {
|
||
const args = process.argv.slice(2);
|
||
|
||
if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
||
showHelp();
|
||
process.exit(0);
|
||
}
|
||
|
||
const command = args[0];
|
||
const options = {
|
||
verbose: false,
|
||
includeTimestamp: true,
|
||
verifyIntegrity: true,
|
||
method: 'backup'
|
||
};
|
||
|
||
let positionalArgs = [];
|
||
|
||
for (let i = 1; i < args.length; i++) {
|
||
const arg = args[i];
|
||
|
||
if (arg.startsWith('--')) {
|
||
const key = arg.slice(2);
|
||
|
||
switch (key) {
|
||
case 'backup-dir':
|
||
options.backupDirectory = args[++i];
|
||
break;
|
||
case 'filename':
|
||
options.filename = args[++i];
|
||
break;
|
||
case 'no-timestamp':
|
||
options.includeTimestamp = false;
|
||
break;
|
||
case 'no-verify':
|
||
options.verifyIntegrity = false;
|
||
break;
|
||
case 'method':
|
||
options.method = args[++i];
|
||
break;
|
||
case 'retention-days':
|
||
options.retentionDays = parseInt(args[++i]);
|
||
break;
|
||
case 'max-backups':
|
||
options.maxBackups = parseInt(args[++i]);
|
||
break;
|
||
case 'target':
|
||
options.targetPath = args[++i];
|
||
break;
|
||
case 'include-checksums':
|
||
options.includeChecksums = true;
|
||
break;
|
||
case 'verbose':
|
||
options.verbose = true;
|
||
break;
|
||
default:
|
||
console.error(`Unknown option: --${key}`);
|
||
process.exit(1);
|
||
}
|
||
} else {
|
||
positionalArgs.push(arg);
|
||
}
|
||
}
|
||
|
||
return { command, args: positionalArgs, options };
|
||
}
|
||
|
||
async function createBackup(databasePath, options) {
|
||
try {
|
||
if (options.verbose) {
|
||
console.log(`🚀 Creating backup for: ${databasePath}`);
|
||
console.log(`📝 Options:`, options);
|
||
} else {
|
||
console.log(`🚀 Creating backup for: ${path.basename(databasePath)}`);
|
||
}
|
||
|
||
const backup = new SQLiteBackup({
|
||
databasePath,
|
||
backupDirectory: options.backupDirectory
|
||
});
|
||
|
||
const result = await backup.createBackup({
|
||
filename: options.filename,
|
||
includeTimestamp: options.includeTimestamp,
|
||
verifyIntegrity: options.verifyIntegrity,
|
||
method: options.method
|
||
});
|
||
|
||
if (result.success) {
|
||
console.log('✅ Backup created successfully!');
|
||
console.log(`📁 Location: ${result.backupPath}`);
|
||
console.log(`📏 Size: ${BackupUtils.formatSize(result.size)}`);
|
||
console.log(`⏱️ Duration: ${BackupUtils.formatDuration(result.duration)}`);
|
||
|
||
if (result.checksum) {
|
||
console.log(`🔐 Checksum: ${result.checksum}`);
|
||
}
|
||
} else {
|
||
console.error('❌ Backup failed:', result.error);
|
||
process.exit(1);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error:', error.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
async function listBackups(databasePath, options) {
|
||
try {
|
||
console.log(`📋 Listing backups for: ${path.basename(databasePath)}`);
|
||
|
||
const backup = new SQLiteBackup({
|
||
databasePath,
|
||
backupDirectory: options.backupDirectory
|
||
});
|
||
|
||
const backups = await backup.listBackups({
|
||
includeChecksums: options.includeChecksums
|
||
});
|
||
|
||
if (backups.length === 0) {
|
||
console.log('ℹ️ No backups found');
|
||
return;
|
||
}
|
||
|
||
console.log(`\nFound ${backups.length} backup(s):\n`);
|
||
|
||
backups.forEach((backup, index) => {
|
||
console.log(`${index + 1}. ${backup.filename}`);
|
||
console.log(` 📁 Path: ${backup.path}`);
|
||
console.log(` 📏 Size: ${BackupUtils.formatSize(backup.size)}`);
|
||
console.log(` 📅 Created: ${backup.created.toISOString()}`);
|
||
|
||
if (options.includeChecksums) {
|
||
console.log(` 🔐 Checksum: ${backup.checksum || 'N/A'}`);
|
||
console.log(` ✅ Valid: ${backup.isValid !== null ? (backup.isValid ? 'Yes' : 'No') : 'Unknown'}`);
|
||
}
|
||
|
||
console.log('');
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error:', error.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
async function cleanupBackups(databasePath, options) {
|
||
try {
|
||
if (!options.retentionDays && !options.maxBackups) {
|
||
console.error('❌ Either --retention-days or --max-backups must be specified');
|
||
process.exit(1);
|
||
}
|
||
|
||
const retentionText = options.retentionDays ?
|
||
`older than ${options.retentionDays} days` :
|
||
`keeping only ${options.maxBackups} most recent`;
|
||
|
||
console.log(`🧹 Cleaning up backups ${retentionText} for: ${path.basename(databasePath)}`);
|
||
|
||
const backup = new SQLiteBackup({
|
||
databasePath,
|
||
backupDirectory: options.backupDirectory
|
||
});
|
||
|
||
const result = await backup.cleanup({
|
||
retentionDays: options.retentionDays,
|
||
maxBackups: options.maxBackups
|
||
});
|
||
|
||
if (result.success) {
|
||
if (result.removed > 0) {
|
||
console.log(`✅ Removed ${result.removed} old backup(s)`);
|
||
|
||
if (options.verbose && result.removedFiles.length > 0) {
|
||
console.log('📁 Removed files:');
|
||
result.removedFiles.forEach(file => console.log(` - ${file}`));
|
||
}
|
||
} else {
|
||
console.log('ℹ️ No old backups to remove');
|
||
}
|
||
|
||
console.log(`📊 Total backups: ${result.totalFiles}, Remaining: ${result.remainingFiles}`);
|
||
|
||
if (result.errors.length > 0) {
|
||
console.warn('⚠️ Some errors occurred:');
|
||
result.errors.forEach(error => console.warn(` ${error}`));
|
||
}
|
||
} else {
|
||
console.error('❌ Cleanup failed:', result.error);
|
||
process.exit(1);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error:', error.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
async function restoreBackup(backupPath, databasePath, options) {
|
||
try {
|
||
console.log(`🔄 Restoring backup: ${path.basename(backupPath)}`);
|
||
console.log(`📍 Target: ${databasePath}`);
|
||
|
||
const backup = new SQLiteBackup({
|
||
databasePath,
|
||
backupDirectory: options.backupDirectory
|
||
});
|
||
|
||
const result = await backup.restore(backupPath, {
|
||
targetPath: options.targetPath || databasePath,
|
||
verifyBefore: options.verifyIntegrity,
|
||
createBackupBeforeRestore: true
|
||
});
|
||
|
||
if (result.success) {
|
||
console.log('✅ Restore completed successfully!');
|
||
console.log(`📁 Restored to: ${result.restoredTo}`);
|
||
|
||
if (result.preRestoreBackup) {
|
||
console.log(`💾 Pre-restore backup: ${result.preRestoreBackup}`);
|
||
}
|
||
} else {
|
||
console.error('❌ Restore failed:', result.error);
|
||
process.exit(1);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error:', error.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
async function verifyBackup(backupPath, options) {
|
||
try {
|
||
console.log(`🔍 Verifying backup: ${path.basename(backupPath)}`);
|
||
|
||
const isValid = await BackupUtils.validateDatabase(backupPath);
|
||
|
||
if (isValid) {
|
||
console.log('✅ Backup is valid');
|
||
} else {
|
||
console.log('❌ Backup is corrupted or invalid');
|
||
process.exit(1);
|
||
}
|
||
|
||
if (options.verbose) {
|
||
const stats = fs.statSync(backupPath);
|
||
console.log(`📏 Size: ${BackupUtils.formatSize(stats.size)}`);
|
||
console.log(`📅 Modified: ${stats.mtime.toISOString()}`);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error:', error.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
const { command, args, options } = parseArgs();
|
||
|
||
try {
|
||
switch (command) {
|
||
case 'create':
|
||
if (args.length !== 1) {
|
||
console.error('❌ Usage: sqlite-backup create <database>');
|
||
process.exit(1);
|
||
}
|
||
await createBackup(args[0], options);
|
||
break;
|
||
|
||
case 'list':
|
||
if (args.length !== 1) {
|
||
console.error('❌ Usage: sqlite-backup list <database>');
|
||
process.exit(1);
|
||
}
|
||
await listBackups(args[0], options);
|
||
break;
|
||
|
||
case 'cleanup':
|
||
if (args.length !== 1) {
|
||
console.error('❌ Usage: sqlite-backup cleanup <database>');
|
||
process.exit(1);
|
||
}
|
||
await cleanupBackups(args[0], options);
|
||
break;
|
||
|
||
case 'restore':
|
||
if (args.length !== 2) {
|
||
console.error('❌ Usage: sqlite-backup restore <backup> <database>');
|
||
process.exit(1);
|
||
}
|
||
await restoreBackup(args[0], args[1], options);
|
||
break;
|
||
|
||
case 'verify':
|
||
if (args.length !== 1) {
|
||
console.error('❌ Usage: sqlite-backup verify <backup>');
|
||
process.exit(1);
|
||
}
|
||
await verifyBackup(args[0], options);
|
||
break;
|
||
|
||
default:
|
||
console.error(`❌ Unknown command: ${command}`);
|
||
showHelp();
|
||
process.exit(1);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Command failed:', error.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// Handle errors
|
||
process.on('unhandledRejection', (error) => {
|
||
console.error('❌ Unhandled promise rejection:', error);
|
||
process.exit(1);
|
||
});
|
||
|
||
process.on('uncaughtException', (error) => {
|
||
console.error('❌ Uncaught exception:', error);
|
||
process.exit(1);
|
||
});
|
||
|
||
// Run the CLI
|
||
if (require.main === module) {
|
||
main();
|
||
}
|
||
|
||
module.exports = { main };
|