commit cf1027d6933d595d5943be51224351f2f1ec8fe1 Author: Derek Anderson Date: Thu Jun 26 20:03:26 2025 -0500 first commit Signed-off-by: Derek Anderson diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..342f916 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,39 @@ +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + reviewers: + - "derekanderson" + assignees: + - "derekanderson" + commit-message: + prefix: "chore" + include: "scope" + labels: + - "dependencies" + - "npm" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + reviewers: + - "derekanderson" + assignees: + - "derekanderson" + commit-message: + prefix: "chore" + include: "scope" + labels: + - "dependencies" + - "github-actions" diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..2c77718 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,208 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + release: + types: [ published ] + +jobs: + test: + name: Test on Node.js ${{ matrix.node-version }} and ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + node-version: [14.x, 16.x, 18.x, 20.x] + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install SQLite (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get update && sudo apt-get install -y sqlite3 + + - name: Install SQLite (macOS) + if: matrix.os == 'macos-latest' + run: brew install sqlite + + - name: Install SQLite (Windows) + if: matrix.os == 'windows-latest' + run: | + choco install sqlite + refreshenv + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Test CLI functionality + run: | + # Test CLI help + node bin/cli.js help + + # Create test database + mkdir -p test-ci + sqlite3 test-ci/test.db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO users (name) VALUES ('Test User');" + + # Test backup creation + node bin/cli.js create test-ci/test.db --backup-dir test-ci/backups + + # Test backup listing + node bin/cli.js list test-ci/test.db --backup-dir test-ci/backups + + # Test backup verification + BACKUP_FILE=$(ls test-ci/backups/*.db | head -1) + node bin/cli.js verify "$BACKUP_FILE" + + # Cleanup + rm -rf test-ci + shell: bash + + - name: Test examples + run: | + # Create sample database for examples + mkdir -p data + sqlite3 data/app.db "CREATE TABLE sample (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO sample (name) VALUES ('Sample Data');" + + # Run examples (but skip the interactive parts) + timeout 30s node examples/basic-usage.js || true + + # Cleanup + rm -rf data + shell: bash + + lint: + name: Lint and Code Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check package structure + run: | + # Verify main files exist + test -f lib/index.js + test -f lib/index.d.ts + test -f bin/cli.js + test -f README.md + test -f package.json + + # Verify CLI is executable + test -x bin/cli.js + + # Verify TypeScript definitions + node -e "const pkg = require('./package.json'); console.log('Package structure OK:', pkg.name, pkg.version)" + + - name: Validate package.json + run: npm pkg fix --dry-run + + - name: Check for vulnerabilities + run: npm audit --audit-level=moderate + + publish: + name: Publish to npm + needs: [test, lint] + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + + - name: Install SQLite + run: sudo apt-get update && sudo apt-get install -y sqlite3 + + - name: Install dependencies + run: npm ci + + - name: Run final tests + run: npm test + + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + update-version: + name: Auto-increment version on main + runs-on: ubuntu-latest + needs: [test, lint] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Configure git + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - name: Bump version + run: | + # Only bump version if not already a version commit + if [[ ! "${{ github.event.head_commit.message }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + npm version patch --no-git-tag-version + git add package.json + git commit -m "$(node -p "require('./package.json').version")" || exit 0 + git push + fi + + security: + name: Security Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + if: always() + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml new file mode 100644 index 0000000..6135e85 --- /dev/null +++ b/.github/workflows/dependencies.yml @@ -0,0 +1,55 @@ +name: Dependencies Update + +on: + schedule: + # Run every Monday at 9 AM UTC + - cron: '0 9 * * 1' + workflow_dispatch: + +jobs: + update-dependencies: + name: Update Dependencies + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install SQLite + run: sudo apt-get update && sudo apt-get install -y sqlite3 + + - name: Check for outdated packages + run: | + npm outdated || true + npm audit || true + + - name: Update packages + run: | + # Update package-lock.json + npm update + + # Run tests to ensure everything still works + npm test + + - name: Create Pull Request + if: success() + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: update dependencies' + title: 'chore: update dependencies' + body: | + Automated dependency update + + - Updated package-lock.json with latest compatible versions + - All tests passing + + Please review and merge if appropriate. + branch: dependencies-update + delete-branch: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ca55611 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,108 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version type to release' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +jobs: + release: + name: Create Release + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + + - name: Install SQLite + run: sudo apt-get update && sudo apt-get install -y sqlite3 + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Configure git + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - name: Bump version and create tag + id: version + run: | + OLD_VERSION=$(node -p "require('./package.json').version") + npm version ${{ github.event.inputs.version }} --no-git-tag-version + NEW_VERSION=$(node -p "require('./package.json').version") + + echo "old_version=$OLD_VERSION" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + + git add package.json + git commit -m "chore: bump version to $NEW_VERSION" + git tag "v$NEW_VERSION" + + - name: Generate changelog + id: changelog + run: | + # Simple changelog generation + echo "CHANGELOG<> $GITHUB_OUTPUT + echo "## Changes in v${{ steps.version.outputs.new_version }}" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + git log --oneline v${{ steps.version.outputs.old_version }}..HEAD --pretty=format:"- %s" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Push changes + run: | + git push origin main + git push origin v${{ steps.version.outputs.new_version }} + + - name: Create GitHub Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.version.outputs.new_version }} + release_name: Release v${{ steps.version.outputs.new_version }} + body: | + ## sqlite-snap v${{ steps.version.outputs.new_version }} + + ${{ steps.changelog.outputs.CHANGELOG }} + + ### Installation + ```bash + npm install -g sqlite-snap@${{ steps.version.outputs.new_version }} + ``` + + ### Usage + ```bash + sqlite-backup create ./database.db + sqlite-backup list ./database.db + sqlite-backup cleanup ./database.db --retention-days 30 + ``` + draft: false + prerelease: false + + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7203ce7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,223 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* +aidocs/ + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# IDEs and editors +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Temporary folders +tmp/ +temp/ + +# Test data and artifacts +test-data/ +test-demo/ +*.test.db +backup-test-* + +# Build artifacts +dist/ +build/ + +# Package files +*.tgz +*.tar.gz diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f43a096 --- /dev/null +++ b/.npmignore @@ -0,0 +1,24 @@ +# Test files and directories +test-data/ +test-demo/ +*.log +aidocs/ + +# Development files +legacy-backup-script.js +integration-example.js +MIGRATION.md + +# Git and IDE files +.git/ +.gitignore +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed32dc8 --- /dev/null +++ b/README.md @@ -0,0 +1,536 @@ +```markdown +[![CI/CD Pipeline](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/ci-cd.yml) +[![Dependencies Update](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/dependencies.yml/badge.svg)](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/dependencies.yml) +[![Security Scan](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/ci-cd.yml/badge.svg?event=schedule)](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/ci-cd.yml) +``` +```markdown +[![npm version](https://badge.fury.io/js/sqlite-snap.svg)](https://badge.fury.io/js/sqlite-snap) +[![npm downloads](https://img.shields.io/npm/dm/sqlite-snap.svg)](https://www.npmjs.com/package/sqlite-snap) +[![npm license](https://img.shields.io/npm/l/sqlite-snap.svg)](https://www.npmjs.com/package/sqlite-snap) +``` +```markdown +[![Node.js supported](https://img.shields.io/badge/node-%3E%3D14.0.0-brightgreen.svg)](https://nodejs.org/) +[![SQLite](https://img.shields.io/badge/SQLite-3.x-blue.svg)](https://www.sqlite.org/) +[![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-green.svg)](https://www.npmjs.com/package/sqlite-snap) +``` + + +# SQLite Backup Library + +A standalone, zero-dependency Node.js library for creating, managing, and verifying SQLite database backups. Perfect for automated backup systems, cron jobs, and database maintenance scripts. + +## Features + +- ๐Ÿš€ **Multiple backup methods**: SQLite backup command, file copy, and vacuum +- โœ… **Backup verification**: Automatic integrity checking using SQLite's built-in PRAGMA +- ๐Ÿงน **Automated cleanup**: Remove old backups based on retention policies +- ๐Ÿ“‹ **Backup management**: List, verify, and restore backups +- ๐Ÿ” **Checksum calculation**: SHA-256 checksums for backup verification +- ๐Ÿ“Š **Detailed reporting**: File sizes, durations, and comprehensive status reporting +- ๐Ÿ› ๏ธ **CLI tool**: Command-line interface for easy scripting and automation +- ๐Ÿ“ฆ **Zero dependencies**: Pure Node.js with no external dependencies + +## Installation + +```bash +npm install sqlite-backup-lib +``` + +Or clone this repository: + +```bash +git clone https://github.com/yourusername/sqlite-backup-lib.git +cd sqlite-backup-lib +npm install +``` + +## Quick Start + +### Programmatic Usage + +```javascript +const { SQLiteBackup, BackupUtils } = require('sqlite-backup-lib'); + +// Initialize backup instance +const backup = new SQLiteBackup({ + databasePath: './data/app.db', + backupDirectory: './backups' +}); + +// Create a backup +const result = await backup.createBackup({ + includeTimestamp: true, + verifyIntegrity: true, + method: 'backup' +}); + +if (result.success) { + console.log(`Backup created: ${result.backupPath}`); + console.log(`Size: ${BackupUtils.formatSize(result.size)}`); +} else { + console.error(`Backup failed: ${result.error}`); +} +``` + +### CLI Usage + +```bash +# Create a backup +sqlite-backup create ./data/app.db + +# Create backup with custom options +sqlite-backup create ./data/app.db --backup-dir ./custom-backups --method copy + +# List all backups +sqlite-backup list ./data/app.db --include-checksums + +# Clean up old backups (keep last 30 days) +sqlite-backup cleanup ./data/app.db --retention-days 30 + +# Restore a backup +sqlite-backup restore ./backups/backup.db ./data/app.db + +# Verify backup integrity +sqlite-backup verify ./backups/backup.db +``` + +## API Reference + +### SQLiteBackup Class + +#### Constructor + +```javascript +const backup = new SQLiteBackup(options) +``` + +**Options:** +- `databasePath` (string, required): Path to the SQLite database file +- `backupDirectory` (string, optional): Directory to store backups (default: `/backups`) +- `createBackupDir` (boolean, optional): Create backup directory if it doesn't exist (default: `true`) + +#### Methods + +##### `createBackup(options)` + +Creates a backup of the SQLite database. + +```javascript +const result = await backup.createBackup({ + filename: 'custom-backup.db', // Custom filename (optional) + includeTimestamp: true, // Include timestamp in filename + verifyIntegrity: true, // Verify backup after creation + method: 'backup' // Backup method: 'backup', 'copy', 'vacuum' +}); +``` + +**Returns:** Promise with backup result + +##### `listBackups(options)` + +Lists all available backups. + +```javascript +const backups = await backup.listBackups({ + pattern: '*.db', // File pattern to match + includeChecksums: false // Calculate checksums (slower) +}); +``` + +**Returns:** Promise of backup information objects + +##### `cleanup(options)` + +Removes old backups based on retention policy. + +```javascript +const result = await backup.cleanup({ + retentionDays: 30, // Keep backups from last 30 days + maxBackups: 10, // Or keep only 10 most recent backups + pattern: '*.db' // File pattern to match +}); +``` + +**Returns:** Promise with cleanup results + +##### `restore(backupPath, options)` + +Restores a backup to the original or specified location. + +```javascript +const result = await backup.restore('./backups/backup.db', { + targetPath: './data/restored.db', // Target path (optional) + verifyBefore: true, // Verify backup before restore + createBackupBeforeRestore: true // Backup current database first +}); +``` + +**Returns:** Promise with restore results + +##### `verifyBackup(backupPath)` + +Verifies the integrity of a backup file. + +```javascript +const isValid = await backup.verifyBackup('./backups/backup.db'); +``` + +**Returns:** Promise + +### BackupUtils Class + +Utility functions for formatting and validation. + +#### Static Methods + +##### `formatSize(bytes)` + +Formats file size in human-readable format. + +```javascript +const size = BackupUtils.formatSize(1048576); // "1.00 MB" +``` + +##### `formatDuration(milliseconds)` + +Formats duration in human-readable format. + +```javascript +const duration = BackupUtils.formatDuration(5000); // "5.00s" +``` + +##### `validateDatabase(databasePath)` + +Validates SQLite database integrity. + +```javascript +const isValid = await BackupUtils.validateDatabase('./data/app.db'); +``` + +## CLI Reference + +### Commands + +#### `create ` + +Creates a backup of the specified database. + +```bash +sqlite-backup create ./data/app.db [options] +``` + +**Options:** +- `--backup-dir `: Directory to store backups +- `--filename `: Custom filename for backup +- `--no-timestamp`: Don't include timestamp in filename +- `--no-verify`: Skip backup verification +- `--method `: Backup method (backup, copy, vacuum) + +#### `list ` + +Lists all backups for the specified database. + +```bash +sqlite-backup list ./data/app.db [options] +``` + +**Options:** +- `--backup-dir `: Directory containing backups +- `--include-checksums`: Include checksums in output (slower) + +#### `cleanup ` + +Cleans up old backups based on retention policy. + +```bash +sqlite-backup cleanup ./data/app.db [options] +``` + +**Options:** +- `--retention-days `: Number of days to keep backups +- `--max-backups `: Maximum number of backups to keep +- `--backup-dir `: Directory containing backups + +#### `restore ` + +Restores a backup to a database. + +```bash +sqlite-backup restore ./backups/backup.db ./data/app.db [options] +``` + +**Options:** +- `--target `: Target path for restore +- `--no-verify`: Skip backup verification before restore + +#### `verify ` + +Verifies backup integrity. + +```bash +sqlite-backup verify ./backups/backup.db [options] +``` + +**Options:** +- `--verbose`: Show detailed information + +### Global Options + +- `--verbose`: Enable verbose output for all commands + +## Backup Methods + +### 1. SQLite Backup (Default) + +Uses SQLite's built-in `.backup` command. This is the recommended method as it creates a consistent backup even while the database is being used. + +```javascript +const result = await backup.createBackup({ method: 'backup' }); +``` + +### 2. File Copy + +Simple file copy operation. Fast but may not be consistent if database is being written to during backup. + +```javascript +const result = await backup.createBackup({ method: 'copy' }); +``` + +### 3. Vacuum + +Uses SQLite's VACUUM command to create a compact backup. Good for reducing file size but slower for large databases. + +```javascript +const result = await backup.createBackup({ method: 'vacuum' }); +``` + +## Examples + +### Basic Automated Backup Script + +```javascript +const { SQLiteBackup } = require('sqlite-backup-lib'); + +async function dailyBackup() { + const backup = new SQLiteBackup({ + databasePath: './data/production.db', + backupDirectory: './backups/daily' + }); + + // Create backup + const result = await backup.createBackup({ + includeTimestamp: true, + verifyIntegrity: true + }); + + if (result.success) { + console.log('โœ… Daily backup completed'); + + // Cleanup old backups (keep 30 days) + await backup.cleanup({ retentionDays: 30 }); + } else { + console.error('โŒ Daily backup failed:', result.error); + // Send alert/notification + } +} + +// Run daily backup +dailyBackup(); +``` + +### Scheduled Backup with Cron + +Create a backup script and schedule it with cron: + +```javascript +// backup-script.js +const { SQLiteBackup, BackupUtils } = require('sqlite-backup-lib'); + +async function scheduledBackup() { + const backup = new SQLiteBackup({ + databasePath: process.env.DB_PATH || './data/app.db', + backupDirectory: process.env.BACKUP_DIR || './backups' + }); + + try { + const result = await backup.createBackup({ + includeTimestamp: true, + verifyIntegrity: true, + method: 'backup' + }); + + if (result.success) { + console.log(`Backup successful: ${BackupUtils.formatSize(result.size)}`); + + // Log to file + const logEntry = { + timestamp: new Date().toISOString(), + success: true, + size: result.size, + path: result.backupPath, + duration: result.duration + }; + + require('fs').appendFileSync('./backup.log', JSON.stringify(logEntry) + '\n'); + + // Cleanup old backups + await backup.cleanup({ retentionDays: 7 }); + + } else { + throw new Error(result.error); + } + + } catch (error) { + console.error('Backup failed:', error.message); + process.exit(1); + } +} + +scheduledBackup(); +``` + +Add to crontab for daily backups at 2 AM: +```bash +0 2 * * * /usr/bin/node /path/to/backup-script.js +``` + +### Backup with Health Monitoring + +```javascript +const { SQLiteBackup, BackupUtils } = require('sqlite-backup-lib'); + +class BackupMonitor { + constructor(config) { + this.backup = new SQLiteBackup(config); + this.alerts = []; + } + + async performBackupWithMonitoring() { + const startTime = Date.now(); + + try { + // Health check first + const isHealthy = await BackupUtils.validateDatabase(this.backup.databasePath); + if (!isHealthy) { + throw new Error('Database failed health check'); + } + + // Create backup + const result = await this.backup.createBackup({ + includeTimestamp: true, + verifyIntegrity: true + }); + + if (!result.success) { + throw new Error(result.error); + } + + // Check backup quality + const backups = await this.backup.listBackups(); + const latestBackup = backups[0]; + + if (latestBackup.size < (result.size * 0.9)) { + this.alerts.push('Warning: Backup size significantly smaller than expected'); + } + + // Cleanup + const cleanupResult = await this.backup.cleanup({ retentionDays: 30 }); + + return { + success: true, + duration: Date.now() - startTime, + backupInfo: result, + cleanupInfo: cleanupResult, + alerts: this.alerts + }; + + } catch (error) { + return { + success: false, + error: error.message, + duration: Date.now() - startTime, + alerts: this.alerts + }; + } + } +} + +// Usage +const monitor = new BackupMonitor({ + databasePath: './data/app.db', + backupDirectory: './monitored-backups' +}); + +monitor.performBackupWithMonitoring().then(result => { + if (result.success) { + console.log('โœ… Monitored backup completed'); + if (result.alerts.length > 0) { + console.warn('โš ๏ธ Alerts:', result.alerts); + } + } else { + console.error('โŒ Monitored backup failed:', result.error); + } +}); +``` + +## Error Handling + +The library provides comprehensive error handling with detailed error messages: + +```javascript +try { + const backup = new SQLiteBackup({ + databasePath: './nonexistent.db' + }); +} catch (error) { + console.error('Initialization error:', error.message); + // "Database file not found: ./nonexistent.db" +} + +const result = await backup.createBackup(); +if (!result.success) { + console.error('Backup error:', result.error); + // Handle backup failure +} +``` + +## Testing + +Run the test suite: + +```bash +npm test +``` + +Run examples: + +```bash +npm run example +``` + +## Requirements + +- Node.js 14.0.0 or higher +- SQLite3 command-line tool installed and available in PATH +- Read/write permissions for database and backup directories + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit a pull request + +## License + +MIT License - see LICENSE file for details. + +## Changelog + +### Version 1.0.0 +- Initial release +- Core backup functionality +- CLI tool +- Comprehensive test suite +- Full documentation diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..1eebe63 --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,381 @@ +#!/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 [options] + +Commands: + create Create a backup of the specified database + list List all backups for the specified database + cleanup Clean up old backups + restore Restore a backup to a database + verify Verify backup integrity + help Show this help message + +Options: + --backup-dir Directory to store backups (default: /backups) + --filename Custom filename for backup + --no-timestamp Don't include timestamp in filename + --no-verify Skip backup verification + --method Backup method: backup, copy, vacuum (default: backup) + --retention-days Number of days to keep backups for cleanup + --max-backups Maximum number of backups to keep + --target 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 '); + process.exit(1); + } + await createBackup(args[0], options); + break; + + case 'list': + if (args.length !== 1) { + console.error('โŒ Usage: sqlite-backup list '); + process.exit(1); + } + await listBackups(args[0], options); + break; + + case 'cleanup': + if (args.length !== 1) { + console.error('โŒ Usage: sqlite-backup cleanup '); + process.exit(1); + } + await cleanupBackups(args[0], options); + break; + + case 'restore': + if (args.length !== 2) { + console.error('โŒ Usage: sqlite-backup restore '); + process.exit(1); + } + await restoreBackup(args[0], args[1], options); + break; + + case 'verify': + if (args.length !== 1) { + console.error('โŒ Usage: sqlite-backup verify '); + 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 }; diff --git a/examples/basic-usage.js b/examples/basic-usage.js new file mode 100644 index 0000000..ef2ff25 --- /dev/null +++ b/examples/basic-usage.js @@ -0,0 +1,275 @@ +const { SQLiteBackup, BackupUtils } = require('../lib/index.js'); +const path = require('path'); + +/** + * Basic usage examples for SQLite Backup Library + */ + +async function basicExample() { + console.log('=== Basic SQLite Backup Example ===\n'); + + try { + // Initialize the backup instance + // Note: Make sure you have a sample database file for testing + const databasePath = './data/app.db'; // Adjust path as needed + + const backup = new SQLiteBackup({ + databasePath: databasePath, + backupDirectory: './data/backups' + }); + + console.log('๐Ÿ“ฆ Creating backup...'); + + // Create a backup + const backupResult = await backup.createBackup({ + includeTimestamp: true, + verifyIntegrity: true, + method: 'backup' + }); + + if (backupResult.success) { + console.log('โœ… Backup created successfully!'); + console.log(`๐Ÿ“ Location: ${backupResult.backupPath}`); + console.log(`๐Ÿ“ Size: ${BackupUtils.formatSize(backupResult.size)}`); + console.log(`โฑ๏ธ Duration: ${BackupUtils.formatDuration(backupResult.duration)}`); + console.log(`๐Ÿ” Checksum: ${backupResult.checksum}`); + } else { + console.error('โŒ Backup failed:', backupResult.error); + return; + } + + console.log('\n๐Ÿ“‹ Listing all backups...'); + + // List all backups + const backups = await backup.listBackups({ + includeChecksums: true + }); + + backups.forEach((backup, index) => { + console.log(`${index + 1}. ${backup.filename}`); + console.log(` ๐Ÿ“ Size: ${BackupUtils.formatSize(backup.size)}`); + console.log(` ๐Ÿ“… Created: ${backup.created.toISOString()}`); + console.log(` โœ… Valid: ${backup.isValid ? 'Yes' : 'No'}`); + }); + + console.log('\n๐Ÿงน Cleaning up old backups (keeping last 5)...'); + + // Cleanup old backups + const cleanupResult = await backup.cleanup({ + maxBackups: 5 + }); + + if (cleanupResult.success) { + console.log(`โœ… Cleanup completed. Removed ${cleanupResult.removed} backups`); + console.log(`๐Ÿ“Š Total: ${cleanupResult.totalFiles}, Remaining: ${cleanupResult.remainingFiles}`); + } else { + console.error('โŒ Cleanup failed:', cleanupResult.error); + } + + } catch (error) { + console.error('โŒ Example failed:', error.message); + } +} + +async function advancedExample() { + console.log('\n=== Advanced SQLite Backup Example ===\n'); + + try { + const databasePath = './data/app.db'; + + const backup = new SQLiteBackup({ + databasePath: databasePath, + backupDirectory: './data/backups' + }); + + console.log('๐Ÿ“ฆ Creating multiple backups with different methods...'); + + // Create backups using different methods + const methods = ['backup', 'copy', 'vacuum']; + const results = []; + + for (const method of methods) { + console.log(`\n๐Ÿ”„ Creating backup using method: ${method}`); + + const result = await backup.createBackup({ + filename: `test-${method}-backup.db`, + includeTimestamp: false, + verifyIntegrity: true, + method: method + }); + + if (result.success) { + console.log(`โœ… ${method} backup created in ${BackupUtils.formatDuration(result.duration)}`); + results.push(result); + } else { + console.error(`โŒ ${method} backup failed:`, result.error); + } + } + + // Compare backup sizes + console.log('\n๐Ÿ“Š Backup comparison:'); + results.forEach(result => { + console.log(`${path.basename(result.backupPath)}: ${BackupUtils.formatSize(result.size)} (${result.method})`); + }); + + // Demonstrate restore functionality + if (results.length > 0) { + console.log('\n๐Ÿ”„ Testing restore functionality...'); + + const testRestorePath = './data/restored-test.db'; + const restoreResult = await backup.restore(results[0].backupPath, { + targetPath: testRestorePath, + verifyBefore: true, + createBackupBeforeRestore: false + }); + + if (restoreResult.success) { + console.log('โœ… Restore test successful'); + console.log(`๐Ÿ“ Restored to: ${restoreResult.restoredTo}`); + + // Clean up test file + const fs = require('fs'); + if (fs.existsSync(testRestorePath)) { + fs.unlinkSync(testRestorePath); + console.log('๐Ÿงน Cleaned up test restore file'); + } + } else { + console.error('โŒ Restore test failed:', restoreResult.error); + } + } + + } catch (error) { + console.error('โŒ Advanced example failed:', error.message); + } +} + +async function scheduledBackupExample() { + console.log('\n=== Scheduled Backup Example ===\n'); + + try { + const databasePath = './data/app.db'; + + const backup = new SQLiteBackup({ + databasePath: databasePath, + backupDirectory: './data/scheduled-backups' + }); + + console.log('โฐ Simulating scheduled backup routine...'); + + // Simulate a scheduled backup that might run daily + const performScheduledBackup = async () => { + const startTime = Date.now(); + + console.log(`\n๐Ÿ”„ Starting scheduled backup at ${new Date().toISOString()}`); + + // Create backup + const backupResult = await backup.createBackup({ + includeTimestamp: true, + verifyIntegrity: true, + method: 'backup' + }); + + if (backupResult.success) { + console.log('โœ… Scheduled backup completed'); + + // Cleanup old backups (keep 7 days) + const cleanupResult = await backup.cleanup({ + retentionDays: 7 + }); + + if (cleanupResult.success && cleanupResult.removed > 0) { + console.log(`๐Ÿงน Cleaned up ${cleanupResult.removed} old backups`); + } + + // Log the backup info (you might want to save this to a log file) + const logEntry = { + timestamp: new Date().toISOString(), + success: true, + duration: BackupUtils.formatDuration(Date.now() - startTime), + size: BackupUtils.formatSize(backupResult.size), + path: backupResult.backupPath, + checksum: backupResult.checksum + }; + + console.log('๐Ÿ“ Backup log entry:', JSON.stringify(logEntry, null, 2)); + + } else { + console.error('โŒ Scheduled backup failed:', backupResult.error); + + // Log the failure + const logEntry = { + timestamp: new Date().toISOString(), + success: false, + error: backupResult.error, + duration: BackupUtils.formatDuration(Date.now() - startTime) + }; + + console.log('๐Ÿ“ Error log entry:', JSON.stringify(logEntry, null, 2)); + } + }; + + // Run the scheduled backup + await performScheduledBackup(); + + console.log('\n๐Ÿ’ก To set up actual scheduled backups, you could:'); + console.log(' 1. Use cron on Linux/macOS: 0 2 * * * /usr/bin/node /path/to/your/backup-script.js'); + console.log(' 2. Use Windows Task Scheduler'); + console.log(' 3. Use a process manager like PM2 with cron jobs'); + console.log(' 4. Use cloud functions with scheduled triggers'); + + } catch (error) { + console.error('โŒ Scheduled backup example failed:', error.message); + } +} + +async function runAllExamples() { + console.log('๐Ÿš€ SQLite Backup Library Examples\n'); + + // Check if database exists + const fs = require('fs'); + const testDbPath = './data/app.db'; + + if (!fs.existsSync(testDbPath)) { + console.log('โš ๏ธ Test database not found. Creating a sample database...'); + + // Create sample database + const { exec } = require('child_process'); + const { promisify } = require('util'); + const execAsync = promisify(exec); + + // Ensure data directory exists + if (!fs.existsSync('./data')) { + fs.mkdirSync('./data', { recursive: true }); + } + + try { + await execAsync(`sqlite3 "${testDbPath}" "CREATE TABLE IF NOT EXISTS sample (id INTEGER PRIMARY KEY, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP); INSERT INTO sample (name) VALUES ('Sample Data 1'), ('Sample Data 2'), ('Sample Data 3');"`); + console.log('โœ… Sample database created'); + } catch (error) { + console.error('โŒ Failed to create sample database:', error.message); + console.log('Please create a test database manually or adjust the database path in the examples'); + return; + } + } + + await basicExample(); + await advancedExample(); + await scheduledBackupExample(); + + console.log('\n๐ŸŽ‰ All examples completed!'); +} + +// Run examples if this file is executed directly +if (require.main === module) { + runAllExamples().catch(error => { + console.error('โŒ Examples failed:', error); + process.exit(1); + }); +} + +module.exports = { + basicExample, + advancedExample, + scheduledBackupExample, + runAllExamples +}; diff --git a/integration-example.js b/integration-example.js new file mode 100644 index 0000000..1f3dccf --- /dev/null +++ b/integration-example.js @@ -0,0 +1,193 @@ +const { SQLiteBackup, BackupUtils } = require('./lib/index.js'); + +/** + * Simple integration example showing how to use the SQLite Backup Library + * This replaces the functionality from the original index.js script + */ + +async function performDatabaseBackup(options = {}) { + const { + databasePath = './data/app.db', + backupDirectory = './data/backups', + method = 'backup', + retention = 30, + verify = true + } = options; + + console.log('๐Ÿš€ Starting database backup...'); + console.log(`๐Ÿ“ Database: ${databasePath}`); + console.log(`๐Ÿ“ Backup directory: ${backupDirectory}`); + console.log(`๐Ÿ”ง Method: ${method}, Retention: ${retention}d, Verify: ${verify}`); + + try { + // Initialize backup instance + const backup = new SQLiteBackup({ + databasePath, + backupDirectory + }); + + // Health check + console.log('๐Ÿ” Performing database health check...'); + const isHealthy = await BackupUtils.validateDatabase(databasePath); + if (!isHealthy) { + throw new Error('Database failed health check'); + } + console.log('โœ… Database health check passed'); + + // Create backup + console.log('๐Ÿ“ฆ Creating backup...'); + const startTime = Date.now(); + + const backupResult = await backup.createBackup({ + includeTimestamp: true, + verifyIntegrity: verify, + method: method + }); + + if (backupResult.success) { + console.log('โœ… Backup created successfully!'); + console.log(`๐Ÿ“ Location: ${backupResult.backupPath}`); + console.log(`๐Ÿ“ Size: ${BackupUtils.formatSize(backupResult.size)}`); + console.log(`๐Ÿ” Checksum: ${backupResult.checksum}`); + console.log(`โฑ๏ธ Duration: ${BackupUtils.formatDuration(backupResult.duration)}`); + } else { + throw new Error(backupResult.error); + } + + // Cleanup old backups + if (retention > 0) { + console.log(`๐Ÿงน Cleaning up backups older than ${retention} days...`); + + const cleanupResult = await backup.cleanup({ + retentionDays: retention + }); + + if (cleanupResult.success) { + if (cleanupResult.removed > 0) { + console.log(`โœ… Removed ${cleanupResult.removed} old backup(s)`); + } else { + console.log('โ„น๏ธ No old backups to remove'); + } + } else { + console.warn('โš ๏ธ Cleanup failed:', cleanupResult.error); + } + } + + console.log('=================================================='); + console.log('โœ… Backup process completed successfully!'); + console.log('=================================================='); + + return { + success: true, + backupInfo: backupResult, + duration: Date.now() - startTime + }; + + } catch (error) { + console.error('โŒ Backup process failed:', error.message); + + console.log('=================================================='); + console.log('โŒ Backup process failed!'); + console.log('=================================================='); + + return { + success: false, + error: error.message + }; + } +} + +// Command line argument parsing (simplified version of original) +function parseSimpleArgs() { + const args = process.argv.slice(2); + const options = { + method: 'backup', + retention: 30, + verify: true + }; + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--method': + options.method = args[++i]; + break; + case '--retention': + options.retention = parseInt(args[++i]); + break; + case '--no-verify': + options.verify = false; + break; + case '--database': + options.databasePath = args[++i]; + break; + case '--backup-dir': + options.backupDirectory = args[++i]; + break; + case '--help': + case '-h': + console.log(` +SQLite Database Backup Tool (Library Version) + +Usage: node integration-example.js [options] + +Options: + --database Path to SQLite database (default: ./data/app.db) + --backup-dir Backup directory (default: ./data/backups) + --method Backup method: backup, copy, vacuum (default: backup) + --retention Days to keep backups (default: 30) + --no-verify Skip backup verification + --help, -h Show this help + +For more advanced usage, use the CLI tool: + sqlite-backup create ./data/app.db --method backup --retention 30 + +Or use the library programmatically - see examples/ directory. + `); + process.exit(0); + break; + } + } + + return options; +} + +// Main execution (compatible with original script usage) +async function main() { + const options = parseSimpleArgs(); + + console.log('=================================================='); + console.log('๐Ÿ“ฆ SQLite Database Backup Tool (Library Version)'); + console.log('=================================================='); + + const result = await performDatabaseBackup(options); + + if (!result.success) { + process.exit(1); + } +} + +// Handle errors (same as original) +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 script if executed directly +if (require.main === module) { + main().catch(error => { + console.error('โŒ Script failed:', error); + process.exit(1); + }); +} + +// Export functions for use as a module +module.exports = { + performDatabaseBackup, + SQLiteBackup, + BackupUtils +}; diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..3f87d2b --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,94 @@ +declare module 'sqlite-backup-lib' { + export interface BackupOptions { + filename?: string; + includeTimestamp?: boolean; + verifyIntegrity?: boolean; + method?: 'backup' | 'copy' | 'vacuum'; + } + + export interface BackupResult { + success: boolean; + backupPath?: string; + filename?: string; + size?: number; + checksum?: string; + duration?: number; + timestamp?: string; + method?: string; + error?: string; + } + + export interface ListBackupOptions { + pattern?: string; + includeChecksums?: boolean; + } + + export interface BackupInfo { + filename: string; + path: string; + size: number; + created: Date; + modified: Date; + isValid?: boolean; + checksum?: string; + } + + export interface CleanupOptions { + retentionDays?: number; + maxBackups?: number; + pattern?: string; + } + + export interface CleanupResult { + success: boolean; + removed: number; + removedFiles?: string[]; + errors?: string[]; + totalFiles?: number; + remainingFiles?: number; + error?: string; + } + + export interface RestoreOptions { + targetPath?: string; + verifyBefore?: boolean; + createBackupBeforeRestore?: boolean; + } + + export interface RestoreResult { + success: boolean; + restoredFrom?: string; + restoredTo?: string; + preRestoreBackup?: string; + timestamp?: string; + error?: string; + } + + export interface SQLiteBackupConfig { + databasePath: string; + backupDirectory?: string; + createBackupDir?: boolean; + } + + export class SQLiteBackup { + constructor(options: SQLiteBackupConfig); + + createBackup(options?: BackupOptions): Promise; + + listBackups(options?: ListBackupOptions): Promise; + + cleanup(options: CleanupOptions): Promise; + + restore(backupPath: string, options?: RestoreOptions): Promise; + + verifyBackup(backupPath: string): Promise; + } + + export class BackupUtils { + static formatSize(bytes: number): string; + + static formatDuration(milliseconds: number): string; + + static validateDatabase(databasePath: string): Promise; + } +} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..21a8093 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,437 @@ +const path = require('path'); +const fs = require('fs'); +const { spawn, exec } = require('child_process'); +const { promisify } = require('util'); +const execAsync = promisify(exec); + +/** + * SQLite Backup Library + * + * A standalone library for creating, managing, and verifying SQLite database backups. + */ +class SQLiteBackup { + /** + * Create a new SQLiteBackup instance + * @param {Object} options - Configuration options + * @param {string} options.databasePath - Path to the SQLite database file + * @param {string} options.backupDirectory - Directory to store backups (default: same directory as database) + * @param {boolean} options.createBackupDir - Create backup directory if it doesn't exist (default: true) + */ + constructor(options = {}) { + if (!options.databasePath) { + throw new Error('Database path is required'); + } + + this.databasePath = path.resolve(options.databasePath); + this.backupDirectory = options.backupDirectory ? + path.resolve(options.backupDirectory) : + path.join(path.dirname(this.databasePath), 'backups'); + this.createBackupDir = options.createBackupDir !== false; + + // Validate database file exists + if (!fs.existsSync(this.databasePath)) { + throw new Error(`Database file not found: ${this.databasePath}`); + } + + // Create backup directory if needed + if (this.createBackupDir && !fs.existsSync(this.backupDirectory)) { + fs.mkdirSync(this.backupDirectory, { recursive: true }); + } + } + + /** + * Create a backup of the SQLite database + * @param {Object} options - Backup options + * @param {string} options.filename - Custom filename for backup (default: auto-generated) + * @param {boolean} options.includeTimestamp - Include timestamp in filename (default: true) + * @param {boolean} options.verifyIntegrity - Verify backup integrity (default: true) + * @param {string} options.method - Backup method: 'backup', 'copy', 'vacuum' (default: 'backup') + * @returns {Promise} Backup result object + */ + async createBackup(options = {}) { + const { + filename, + includeTimestamp = true, + verifyIntegrity = true, + method = 'backup' + } = options; + + try { + const startTime = Date.now(); + + // Generate backup filename + const backupFileName = this._generateBackupFilename(filename, includeTimestamp); + const backupPath = path.join(this.backupDirectory, backupFileName); + + // Create backup based on method + await this._performBackup(method, backupPath); + + // Verify backup integrity if requested + if (verifyIntegrity) { + const isValid = await this.verifyBackup(backupPath); + if (!isValid) { + fs.unlinkSync(backupPath); + throw new Error('Backup failed integrity check'); + } + } + + // Get backup file stats + const stats = fs.statSync(backupPath); + const checksum = await this._calculateChecksum(backupPath); + const duration = Date.now() - startTime; + + const result = { + success: true, + backupPath, + filename: backupFileName, + size: stats.size, + checksum, + duration, + timestamp: new Date().toISOString(), + method + }; + + return result; + + } catch (error) { + return { + success: false, + error: error.message, + timestamp: new Date().toISOString() + }; + } + } + + /** + * Verify the integrity of a backup file + * @param {string} backupPath - Path to the backup file + * @returns {Promise} True if backup is valid + */ + async verifyBackup(backupPath) { + try { + // Check if file exists first + if (!fs.existsSync(backupPath)) { + return false; + } + + const command = `sqlite3 "${backupPath}" "PRAGMA integrity_check;"`; + const { stdout } = await execAsync(command); + return stdout.trim() === 'ok'; + } catch (error) { + return false; + } + } + + /** + * Clean up old backup files based on retention policy + * @param {Object} options - Cleanup options + * @param {number} options.retentionDays - Number of days to keep backups + * @param {number} options.maxBackups - Maximum number of backups to keep (alternative to retentionDays) + * @param {string} options.pattern - File pattern to match (default: '*.db') + * @returns {Promise} Cleanup result object + */ + async cleanup(options = {}) { + const { + retentionDays, + maxBackups, + pattern = '*.db' + } = options; + + if (!retentionDays && !maxBackups) { + throw new Error('Either retentionDays or maxBackups must be specified'); + } + + try { + const files = this._getBackupFiles(pattern); + let filesToRemove = []; + + if (retentionDays) { + const cutoffDate = new Date(Date.now() - (retentionDays * 24 * 60 * 60 * 1000)); + filesToRemove = files.filter(file => file.stats.mtime < cutoffDate); + } else if (maxBackups) { + // Sort by modification time (newest first) and keep only maxBackups + files.sort((a, b) => b.stats.mtime - a.stats.mtime); + filesToRemove = files.slice(maxBackups); + } + + const removed = []; + const errors = []; + + for (const file of filesToRemove) { + try { + fs.unlinkSync(file.path); + removed.push(file.name); + } catch (error) { + errors.push(`Failed to remove ${file.name}: ${error.message}`); + } + } + + return { + success: true, + removed: removed.length, + removedFiles: removed, + errors, + totalFiles: files.length, + remainingFiles: files.length - removed.length + }; + + } catch (error) { + return { + success: false, + error: error.message, + removed: 0, + errors: [error.message] + }; + } + } + + /** + * Get information about existing backups + * @param {Object} options - List options + * @param {string} options.pattern - File pattern to match (default: '*.db') + * @param {boolean} options.includeChecksums - Calculate checksums for each backup (default: false) + * @returns {Promise} Array of backup information objects + */ + async listBackups(options = {}) { + const { + pattern = '*.db', + includeChecksums = false + } = options; + + try { + const files = this._getBackupFiles(pattern); + const backups = []; + + for (const file of files) { + const backup = { + filename: file.name, + path: file.path, + size: file.stats.size, + created: file.stats.birthtime, + modified: file.stats.mtime, + isValid: null, + checksum: null + }; + + if (includeChecksums) { + backup.checksum = await this._calculateChecksum(file.path); + backup.isValid = await this.verifyBackup(file.path); + } + + backups.push(backup); + } + + // Sort by creation time (newest first) + backups.sort((a, b) => b.created - a.created); + + return backups; + + } catch (error) { + throw new Error(`Failed to list backups: ${error.message}`); + } + } + + /** + * Restore a backup to the original database location or a new location + * @param {string} backupPath - Path to the backup file + * @param {Object} options - Restore options + * @param {string} options.targetPath - Target path for restore (default: original database path) + * @param {boolean} options.verifyBefore - Verify backup before restore (default: true) + * @param {boolean} options.createBackupBeforeRestore - Create backup of current database before restore (default: true) + * @returns {Promise} Restore result object + */ + async restore(backupPath, options = {}) { + const { + targetPath = this.databasePath, + verifyBefore = true, + createBackupBeforeRestore = true + } = options; + + try { + // Verify backup before restore + if (verifyBefore) { + const isValid = await this.verifyBackup(backupPath); + if (!isValid) { + throw new Error('Backup file failed integrity check'); + } + } + + let currentBackupPath = null; + + // Create backup of current database if requested + if (createBackupBeforeRestore && fs.existsSync(targetPath)) { + const currentBackupResult = await this.createBackup({ + filename: `pre-restore-backup-${Date.now()}.db`, + includeTimestamp: false + }); + + if (currentBackupResult.success) { + currentBackupPath = currentBackupResult.backupPath; + } else { + throw new Error(`Failed to create pre-restore backup: ${currentBackupResult.error}`); + } + } + + // Perform restore (copy backup to target location) + fs.copyFileSync(backupPath, targetPath); + + // Verify restored database + const restoredIsValid = await this.verifyBackup(targetPath); + if (!restoredIsValid) { + throw new Error('Restored database failed integrity check'); + } + + return { + success: true, + restoredFrom: backupPath, + restoredTo: targetPath, + preRestoreBackup: currentBackupPath, + timestamp: new Date().toISOString() + }; + + } catch (error) { + return { + success: false, + error: error.message, + timestamp: new Date().toISOString() + }; + } + } + + // Private methods + + _generateBackupFilename(customFilename, includeTimestamp) { + if (customFilename) { + return customFilename.endsWith('.db') ? customFilename : `${customFilename}.db`; + } + + const baseName = path.basename(this.databasePath, '.db'); + const timestamp = includeTimestamp ? + `-${new Date().toISOString().replace(/[:.]/g, '-')}` : ''; + + return `${baseName}-backup${timestamp}.db`; + } + + async _performBackup(method, backupPath) { + switch (method) { + case 'backup': + return this._backupUsingBackupCommand(backupPath); + case 'copy': + return this._backupUsingCopy(backupPath); + case 'vacuum': + return this._backupUsingVacuum(backupPath); + default: + throw new Error(`Unknown backup method: ${method}`); + } + } + + async _backupUsingBackupCommand(backupPath) { + const command = `sqlite3 "${this.databasePath}" ".backup '${backupPath}'"`; + await execAsync(command); + } + + async _backupUsingCopy(backupPath) { + fs.copyFileSync(this.databasePath, backupPath); + } + + async _backupUsingVacuum(backupPath) { + const command = `sqlite3 "${this.databasePath}" ".backup '${backupPath}'" ".exit"`; + await execAsync(command); + } + + async _calculateChecksum(filePath) { + try { + const { stdout } = await execAsync(`shasum -a 256 "${filePath}"`); + return stdout.split(' ')[0]; + } catch (error) { + return null; + } + } + + _getBackupFiles(pattern) { + if (!fs.existsSync(this.backupDirectory)) { + return []; + } + + const files = fs.readdirSync(this.backupDirectory); + const backupFiles = []; + + for (const filename of files) { + if (this._matchesPattern(filename, pattern)) { + const filePath = path.join(this.backupDirectory, filename); + const stats = fs.statSync(filePath); + + backupFiles.push({ + name: filename, + path: filePath, + stats + }); + } + } + + return backupFiles; + } + + _matchesPattern(filename, pattern) { + // Simple pattern matching - could be enhanced with a proper glob library + if (pattern === '*' || pattern === '*.*') return true; + if (pattern.startsWith('*.')) { + const extension = pattern.slice(2); + return filename.endsWith('.' + extension); + } + return filename === pattern; + } +} + +/** + * Utility functions for format and helpers + */ +class BackupUtils { + /** + * Format file size in human readable format + * @param {number} bytes - Size in bytes + * @returns {string} Formatted size string + */ + static formatSize(bytes) { + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) return '0 B'; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`; + } + + /** + * Format duration in human readable format + * @param {number} milliseconds - Duration in milliseconds + * @returns {string} Formatted duration string + */ + static formatDuration(milliseconds) { + if (milliseconds < 1000) return `${milliseconds}ms`; + if (milliseconds < 60000) return `${(milliseconds / 1000).toFixed(2)}s`; + return `${(milliseconds / 60000).toFixed(2)}m`; + } + + /** + * Validate SQLite database file + * @param {string} databasePath - Path to database file + * @returns {Promise} True if database is valid + */ + static async validateDatabase(databasePath) { + try { + // Check if file exists first + if (!require('fs').existsSync(databasePath)) { + return false; + } + + const command = `sqlite3 "${databasePath}" "PRAGMA integrity_check;"`; + const { stdout } = await execAsync(command); + return stdout.trim() === 'ok'; + } catch (error) { + return false; + } + } +} + +module.exports = { + SQLiteBackup, + BackupUtils +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..480f12c --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "sqlite-snap", + "version": "1.0.0", + "description": "A standalone library for SQLite database backups with cleanup and verification features", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "bin": { + "sqlite-backup": "bin/cli.js" + }, + "scripts": { + "test": "node test/test.js", + "test:ci": "npm test", + "example": "node examples/basic-usage.js", + "prepublishOnly": "npm test", + "preversion": "npm test", + "postversion": "git push && git push --tags", + "cli": "node bin/cli.js", + "demo": "npm run cli -- help" + }, + "keywords": [ + "sqlite", + "backup", + "database", + "sqlite3", + "automation", + "cron" + ], + "author": "Derek Anderson", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "dependencies": {}, + "devDependencies": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/derekanderson/sqlite-backup-lib.git" + }, + "bugs": { + "url": "https://github.com/derekanderson/sqlite-backup-lib/issues" + }, + "homepage": "https://github.com/derekanderson/sqlite-backup-lib#readme" +} \ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..4270d2d --- /dev/null +++ b/test/test.js @@ -0,0 +1,413 @@ +const { SQLiteBackup, BackupUtils } = require('../lib/index.js'); +const path = require('path'); +const fs = require('fs'); +const { exec } = require('child_process'); +const { promisify } = require('util'); +const execAsync = promisify(exec); + +/** + * Simple test suite for SQLite Backup Library + */ + +class TestRunner { + constructor() { + this.tests = []; + this.passed = 0; + this.failed = 0; + } + + test(name, testFn) { + this.tests.push({ name, testFn }); + } + + async run() { + console.log('๐Ÿงช Running SQLite Backup Library Tests\n'); + + for (const test of this.tests) { + try { + console.log(`๐Ÿ” Testing: ${test.name}`); + await test.testFn(); + console.log(`โœ… PASS: ${test.name}\n`); + this.passed++; + } catch (error) { + console.error(`โŒ FAIL: ${test.name}`); + console.error(` Error: ${error.message}\n`); + this.failed++; + } + } + + console.log('๐Ÿ“Š Test Results:'); + console.log(` โœ… Passed: ${this.passed}`); + console.log(` โŒ Failed: ${this.failed}`); + console.log(` ๐Ÿ“ˆ Total: ${this.tests.length}`); + + if (this.failed > 0) { + process.exit(1); + } + } +} + +// Test utilities +function assert(condition, message) { + if (!condition) { + throw new Error(message || 'Assertion failed'); + } +} + +function assertEquals(actual, expected, message) { + if (actual !== expected) { + throw new Error(message || `Expected ${expected}, got ${actual}`); + } +} + +function assertExists(filePath, message) { + if (!fs.existsSync(filePath)) { + throw new Error(message || `File does not exist: ${filePath}`); + } +} + +// Setup and teardown +async function setupTestEnvironment() { + const testDir = './test-data'; + const dbPath = path.join(testDir, 'test.db'); + const backupDir = path.join(testDir, 'backups'); + + // Clean up any existing test data + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }); + } + + // Create test directory + fs.mkdirSync(testDir, { recursive: true }); + + // Create test database + await execAsync(`sqlite3 "${dbPath}" "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT); INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com'), ('Jane Smith', 'jane@example.com');"`); + + return { testDir, dbPath, backupDir }; +} + +function cleanupTestEnvironment(testDir) { + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }); + } +} + +// Initialize test runner +const runner = new TestRunner(); + +// Test: Basic backup creation +runner.test('Basic backup creation', async () => { + const { testDir, dbPath, backupDir } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: backupDir + }); + + const result = await backup.createBackup({ + filename: 'test-backup.db', + includeTimestamp: false, + verifyIntegrity: true + }); + + assert(result.success, 'Backup should succeed'); + assertExists(result.backupPath, 'Backup file should exist'); + assert(result.size > 0, 'Backup should have positive size'); + assert(result.checksum, 'Backup should have checksum'); + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: Backup with timestamp +runner.test('Backup with timestamp', async () => { + const { testDir, dbPath, backupDir } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: backupDir + }); + + const result = await backup.createBackup({ + includeTimestamp: true, + verifyIntegrity: true + }); + + assert(result.success, 'Backup should succeed'); + assert(result.filename.includes('-'), 'Filename should include timestamp'); + assertExists(result.backupPath, 'Backup file should exist'); + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: Different backup methods +runner.test('Different backup methods', async () => { + const { testDir, dbPath, backupDir } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: backupDir + }); + + const methods = ['backup', 'copy']; + + for (const method of methods) { + const result = await backup.createBackup({ + filename: `test-${method}.db`, + includeTimestamp: false, + method: method + }); + + assert(result.success, `${method} backup should succeed`); + assertEquals(result.method, method, `Method should be ${method}`); + assertExists(result.backupPath, `${method} backup file should exist`); + } + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: Backup verification +runner.test('Backup verification', async () => { + const { testDir, dbPath, backupDir } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: backupDir + }); + + const result = await backup.createBackup({ + filename: 'verify-test.db', + includeTimestamp: false, + verifyIntegrity: true + }); + + assert(result.success, 'Backup should succeed'); + + const isValid = await backup.verifyBackup(result.backupPath); + assert(isValid, 'Backup should be valid'); + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: List backups +runner.test('List backups', async () => { + const { testDir, dbPath, backupDir } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: backupDir + }); + + // Create multiple backups + await backup.createBackup({ filename: 'backup1.db', includeTimestamp: false }); + await backup.createBackup({ filename: 'backup2.db', includeTimestamp: false }); + await backup.createBackup({ filename: 'backup3.db', includeTimestamp: false }); + + const backups = await backup.listBackups({ + includeChecksums: true + }); + + assertEquals(backups.length, 3, 'Should have 3 backups'); + + backups.forEach(backup => { + assert(backup.filename, 'Backup should have filename'); + assert(backup.path, 'Backup should have path'); + assert(backup.size > 0, 'Backup should have positive size'); + assert(backup.created, 'Backup should have creation date'); + }); + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: Cleanup by retention days +runner.test('Cleanup by retention days', async () => { + const { testDir, dbPath, backupDir } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: backupDir + }); + + // Create a backup + const result = await backup.createBackup({ + filename: 'old-backup.db', + includeTimestamp: false + }); + + // Manually change the file's modification time to make it "old" + const oldTime = new Date(Date.now() - (10 * 24 * 60 * 60 * 1000)); // 10 days ago + fs.utimesSync(result.backupPath, oldTime, oldTime); + + // Run cleanup with 7 day retention + const cleanupResult = await backup.cleanup({ + retentionDays: 7 + }); + + assert(cleanupResult.success, 'Cleanup should succeed'); + assertEquals(cleanupResult.removed, 1, 'Should remove 1 old backup'); + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: Cleanup by max backups +runner.test('Cleanup by max backups', async () => { + const { testDir, dbPath, backupDir } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: backupDir + }); + + // Create 5 backups + for (let i = 1; i <= 5; i++) { + await backup.createBackup({ + filename: `backup${i}.db`, + includeTimestamp: false + }); + + // Small delay to ensure different modification times + await new Promise(resolve => setTimeout(resolve, 100)); + } + + // Keep only 3 most recent + const cleanupResult = await backup.cleanup({ + maxBackups: 3 + }); + + assert(cleanupResult.success, 'Cleanup should succeed'); + assertEquals(cleanupResult.removed, 2, 'Should remove 2 old backups'); + assertEquals(cleanupResult.remainingFiles, 3, 'Should have 3 remaining files'); + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: Restore backup +runner.test('Restore backup', async () => { + const { testDir, dbPath, backupDir } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: backupDir + }); + + // Create backup + const backupResult = await backup.createBackup({ + filename: 'restore-test.db', + includeTimestamp: false + }); + + assert(backupResult.success, 'Backup creation should succeed'); + + // Restore to a new location + const restorePath = path.join(testDir, 'restored.db'); + const restoreResult = await backup.restore(backupResult.backupPath, { + targetPath: restorePath, + createBackupBeforeRestore: false + }); + + assert(restoreResult.success, 'Restore should succeed'); + assertEquals(restoreResult.restoredTo, restorePath, 'Should restore to correct path'); + assertExists(restorePath, 'Restored file should exist'); + + // Verify restored database has same content + const isValid = await BackupUtils.validateDatabase(restorePath); + assert(isValid, 'Restored database should be valid'); + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: BackupUtils functions +runner.test('BackupUtils functions', async () => { + // Test formatSize + assertEquals(BackupUtils.formatSize(0), '0 B', 'Should format 0 bytes'); + assertEquals(BackupUtils.formatSize(1024), '1.00 KB', 'Should format KB'); + assertEquals(BackupUtils.formatSize(1048576), '1.00 MB', 'Should format MB'); + + // Test formatDuration + assertEquals(BackupUtils.formatDuration(500), '500ms', 'Should format milliseconds'); + assertEquals(BackupUtils.formatDuration(5000), '5.00s', 'Should format seconds'); + assertEquals(BackupUtils.formatDuration(120000), '2.00m', 'Should format minutes'); + + // Test validateDatabase + const { testDir, dbPath } = await setupTestEnvironment(); + + try { + const isValid = await BackupUtils.validateDatabase(dbPath); + assert(isValid, 'Valid database should return true'); + + // Test with non-existent file + const nonExistentPath = './test-non-existent-' + Date.now() + '.db'; + const invalidResult = await BackupUtils.validateDatabase(nonExistentPath); + assert(!invalidResult, 'Non-existent database should return false'); + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Test: Error handling +runner.test('Error handling', async () => { + // Test with non-existent database + const nonExistentPath = './test-non-existent-' + Date.now() + '.db'; + try { + new SQLiteBackup({ + databasePath: nonExistentPath + }); + assert(false, 'Should throw error for non-existent database'); + } catch (error) { + assert(error.message.includes('not found'), 'Should throw appropriate error'); + } + + // Test cleanup without retention options + const { testDir, dbPath } = await setupTestEnvironment(); + + try { + const backup = new SQLiteBackup({ + databasePath: dbPath, + backupDirectory: path.join(testDir, 'backups') + }); + + try { + await backup.cleanup({}); + assert(false, 'Should throw error without retention options'); + } catch (error) { + assert(error.message.includes('retentionDays or maxBackups'), 'Should throw appropriate error'); + } + + } finally { + cleanupTestEnvironment(testDir); + } +}); + +// Run all tests +if (require.main === module) { + runner.run().catch(error => { + console.error('โŒ Test runner failed:', error); + process.exit(1); + }); +} + +module.exports = { runner };