Derek Anderson cf1027d693
first commit
Signed-off-by: Derek Anderson <dmikey@users.noreply.github.com>
2025-06-26 20:03:26 -05:00

382 lines
12 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 };