first commit

Signed-off-by: Derek Anderson <dmikey@users.noreply.github.com>
This commit is contained in:
Derek Anderson 2025-06-26 20:03:26 -05:00
commit cf1027d693
No known key found for this signature in database
14 changed files with 3029 additions and 0 deletions

39
.github/dependabot.yml vendored Normal file
View File

@ -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"

208
.github/workflows/ci-cd.yml vendored Normal file
View File

@ -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'

55
.github/workflows/dependencies.yml vendored Normal file
View File

@ -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

108
.github/workflows/release.yml vendored Normal file
View File

@ -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<<EOF" >> $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 }}

223
.gitignore vendored Normal file
View File

@ -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

24
.npmignore Normal file
View File

@ -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

536
README.md Normal file
View File

@ -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: `<database-dir>/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<Object> 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<Array> 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<Object> 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<Object> with restore results
##### `verifyBackup(backupPath)`
Verifies the integrity of a backup file.
```javascript
const isValid = await backup.verifyBackup('./backups/backup.db');
```
**Returns:** Promise<boolean>
### 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 <database>`
Creates a backup of the specified database.
```bash
sqlite-backup create ./data/app.db [options]
```
**Options:**
- `--backup-dir <dir>`: Directory to store 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)
#### `list <database>`
Lists all backups for the specified database.
```bash
sqlite-backup list ./data/app.db [options]
```
**Options:**
- `--backup-dir <dir>`: Directory containing backups
- `--include-checksums`: Include checksums in output (slower)
#### `cleanup <database>`
Cleans up old backups based on retention policy.
```bash
sqlite-backup cleanup ./data/app.db [options]
```
**Options:**
- `--retention-days <days>`: Number of days to keep backups
- `--max-backups <number>`: Maximum number of backups to keep
- `--backup-dir <dir>`: Directory containing backups
#### `restore <backup> <database>`
Restores a backup to a database.
```bash
sqlite-backup restore ./backups/backup.db ./data/app.db [options]
```
**Options:**
- `--target <path>`: Target path for restore
- `--no-verify`: Skip backup verification before restore
#### `verify <backup>`
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

381
bin/cli.js Executable file
View File

@ -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 <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 };

275
examples/basic-usage.js Normal file
View File

@ -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
};

193
integration-example.js Normal file
View File

@ -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> Path to SQLite database (default: ./data/app.db)
--backup-dir <path> Backup directory (default: ./data/backups)
--method <method> Backup method: backup, copy, vacuum (default: backup)
--retention <days> 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
};

94
lib/index.d.ts vendored Normal file
View File

@ -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<BackupResult>;
listBackups(options?: ListBackupOptions): Promise<BackupInfo[]>;
cleanup(options: CleanupOptions): Promise<CleanupResult>;
restore(backupPath: string, options?: RestoreOptions): Promise<RestoreResult>;
verifyBackup(backupPath: string): Promise<boolean>;
}
export class BackupUtils {
static formatSize(bytes: number): string;
static formatDuration(milliseconds: number): string;
static validateDatabase(databasePath: string): Promise<boolean>;
}
}

437
lib/index.js Normal file
View File

@ -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<Object>} 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<boolean>} 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<Object>} 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>} 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<Object>} 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<boolean>} 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
};

43
package.json Normal file
View File

@ -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"
}

413
test/test.js Normal file
View File

@ -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 };