#!/usr/bin/env bash # # Copy the quartermaster SQLite database into backups/ before any schema # change, data migration, or destructive operation. Safe to run while the # application is writing to the DB: uses Python's sqlite3.Connection.backup # (the online backup API), not a plain file copy. # # Usage: # scripts/backup-db.sh [reason] # # `reason` becomes part of the backup filename. Defaults to "manual". # # Resolves the database path in this order: # 1. QUARTERMASTER_DB_URL if it starts with sqlite:/// (uses the path part) # 2. ./quartermaster.db relative to the repo root # # Exit status: # 0 backup written, or source DB does not exist (nothing to do) # 1 backup failed, or a non-file sqlite URL was supplied set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repo_root" reason="${1:-manual}" exec python3 - "$reason" <<'PY' import os import re import sqlite3 import sys from datetime import datetime from pathlib import Path reason = sys.argv[1] db_path = Path("quartermaster.db") url = os.environ.get("QUARTERMASTER_DB_URL") if url: if url.startswith("sqlite:///"): db_path = Path(url[len("sqlite:///"):]) elif url.startswith("sqlite://"): print(f"backup-db.sh: non-file sqlite URL not supported: {url}", file=sys.stderr) sys.exit(1) else: print(f"backup-db.sh: QUARTERMASTER_DB_URL is not a sqlite URL: {url}", file=sys.stderr) sys.exit(1) if not db_path.is_file(): print(f"backup-db.sh: no database at {db_path}, nothing to back up.") sys.exit(0) slug = re.sub(r"[^A-Za-z0-9]+", "-", reason).strip("-") or "manual" timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") backup_root = os.environ.get("QUARTERMASTER_BACKUP_DIR") if backup_root: dest_dir = Path(backup_root) else: dest_dir = db_path.resolve().parent / "backups" dest_dir.mkdir(parents=True, exist_ok=True) dest = dest_dir / f"quartermaster-{timestamp}-{slug}.db" src = sqlite3.connect(str(db_path)) try: dst = sqlite3.connect(str(dest)) try: src.backup(dst) finally: dst.close() finally: src.close() print(f"backup-db.sh: {db_path} -> {dest} ({dest.stat().st_size} bytes)") PY