first commit
Signed-off-by: Derek Anderson <dmikey@users.noreply.github.com>
This commit is contained in:
commit
cf1027d693
39
.github/dependabot.yml
vendored
Normal file
39
.github/dependabot.yml
vendored
Normal 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
208
.github/workflows/ci-cd.yml
vendored
Normal 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
55
.github/workflows/dependencies.yml
vendored
Normal 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
108
.github/workflows/release.yml
vendored
Normal 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
223
.gitignore
vendored
Normal 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
24
.npmignore
Normal 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
536
README.md
Normal file
@ -0,0 +1,536 @@
|
||||
```markdown
|
||||
[](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/ci-cd.yml)
|
||||
[](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/dependencies.yml)
|
||||
[](https://github.com/derekanderson/sqlite-backup-lib/actions/workflows/ci-cd.yml)
|
||||
```
|
||||
```markdown
|
||||
[](https://badge.fury.io/js/sqlite-snap)
|
||||
[](https://www.npmjs.com/package/sqlite-snap)
|
||||
[](https://www.npmjs.com/package/sqlite-snap)
|
||||
```
|
||||
```markdown
|
||||
[](https://nodejs.org/)
|
||||
[](https://www.sqlite.org/)
|
||||
[](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
381
bin/cli.js
Executable 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
275
examples/basic-usage.js
Normal 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
193
integration-example.js
Normal 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
94
lib/index.d.ts
vendored
Normal 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
437
lib/index.js
Normal 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
43
package.json
Normal 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
413
test/test.js
Normal 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 };
|
||||
Loading…
x
Reference in New Issue
Block a user