diff --git a/scripts/maven-backup.sh b/scripts/maven-backup.sh new file mode 100755 index 0000000..3f96dcf --- /dev/null +++ b/scripts/maven-backup.sh @@ -0,0 +1,250 @@ +#!/bin/bash +# maven-backup.sh — backup/restore/verify the encrypted Maven SQLite database. +# +# The encrypted DB (AES-256-GCM with 6-byte magic "MVNC1\0") is the only +# persistent state. Backup is `cp` + magic-verify; restore is `cp` back. +# +# Usage: +# maven-backup.sh backup — create timestamped backup +# maven-backup.sh restore — restore from backup +# maven-backup.sh verify [backup-file] — verify magic header (default: latest) +# maven-backup.sh list — list available backups + +set -euo pipefail + +SCRIPT_NAME=$(basename "$0") +CONFIG_FILE="${MAVEN_CONFIG:-/opt/maven/config/mavend.json}" +BACKUP_DIR="${MAVEN_BACKUP_DIR:-/var/backups/maven}" +STATE_DIR="${MAVEN_STATE_DIR:-/var/lib/maven}" +DB_FILE="${MAVEN_DB_FILE:-maven.db.enc}" +DB_PATH="$STATE_DIR/$DB_FILE" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +usage() { + cat < Restore the DB from a backup file + verify [backup-file] Verify backup integrity (default: latest) + list List available backups + +Environment: + MAVEN_CONFIG Path to mavend.json (default: /opt/maven/config/mavend.json) + MAVEN_BACKUP_DIR Backup directory (default: /var/backups/maven) + MAVEN_STATE_DIR State directory with the DB file (default: /var/lib/maven) + MAVEN_DB_FILE Database filename (default: maven.db.enc) +EOF + exit 1 +} + +# check_magic: verify the file starts with the correct 6-byte magic header. +# Magic is "MVNC1\0" = bytes: 4d 56 4e 43 31 00 +check_magic() { + local file="$1" + if [ ! -f "$file" ]; then + error "file not found: $file" + return 1 + fi + local magic + magic=$(od -A n -t x1 -N 6 "$file" 2>/dev/null | tr -d ' \n') + if [ "$magic" = "4d564e433100" ]; then + return 0 + fi + error "invalid magic header in $file (expected MVNC1\\0)" + return 1 +} + +# file_size: get file size in bytes (Linux stat). +file_size() { + local file="$1" + stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "0" +} + +# read_config_value: read a JSON string value from the config file using grep. +read_config_value() { + local key="$1" + grep -o "\"$key\": *\"[^\"]*\"" "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/.*: *"\(.*\)"/\1/' +} + +# resolve_db_path: determine the actual DB path from config or defaults. +resolve_db_path() { + local config_db + config_db=$(read_config_value "db_path") + if [ -n "$config_db" ]; then + echo "$config_db" + else + echo "$DB_PATH" + fi +} + +cmd_backup() { + local actual_db + actual_db=$(resolve_db_path) + + if [ ! -f "$actual_db" ]; then + error "database not found at $actual_db" + error "is mavend running? has it been started at least once?" + exit 1 + fi + + mkdir -p "$BACKUP_DIR" + local timestamp + timestamp=$(date -u +"%Y%m%dT%H%M%SZ") + local backup_file="$BACKUP_DIR/maven-db-$timestamp.enc" + + info "backing up $actual_db → $backup_file" + cp "$actual_db" "$backup_file" + sync + + if check_magic "$backup_file"; then + local size + size=$(file_size "$backup_file") + info "backup complete: $backup_file ($size bytes)" + echo "$backup_file" + else + error "backup file failed magic check — removing" + rm -f "$backup_file" + exit 1 + fi +} + +cmd_restore() { + local backup_file="$1" + local actual_db + actual_db=$(resolve_db_path) + + if [ ! -f "$backup_file" ]; then + error "backup file not found: $backup_file" + exit 1 + fi + + if ! check_magic "$backup_file"; then + error "backup file is not a valid encrypted Maven database" + exit 1 + fi + + if [ -f "$actual_db" ]; then + local backup_size restore_size + backup_size=$(file_size "$backup_file") + restore_size=$(file_size "$actual_db") + warn "about to OVERWRITE $actual_db ($restore_size bytes)" + warn "with $backup_file ($backup_size bytes)" + echo -n "continue? [y/N] " + read -r confirm + if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + info "restore cancelled" + exit 0 + fi + fi + + info "restoring $backup_file → $actual_db" + cp "$backup_file" "$actual_db" + sync + + if check_magic "$actual_db"; then + info "restore complete: $actual_db" + else + error "restored file failed magic check — database may be corrupt" + exit 1 + fi +} + +cmd_verify() { + local target="$1" + + if [ ! -f "$target" ]; then + error "file not found: $target" + exit 1 + fi + + if check_magic "$target"; then + local size + size=$(file_size "$target") + info "valid encrypted database: $target ($size bytes)" + return 0 + else + error "NOT a valid encrypted database: $target" + exit 1 + fi +} + +cmd_list() { + if [ ! -d "$BACKUP_DIR" ]; then + info "no backups directory: $BACKUP_DIR" + exit 0 + fi + + local backups + backups=$(ls -1 "$BACKUP_DIR"/maven-db-*.enc 2>/dev/null || true) + if [ -z "$backups" ]; then + info "no backups found in $BACKUP_DIR" + exit 0 + fi + + echo "Available backups in $BACKUP_DIR:" + echo "" + printf "%-30s %12s %s\n" "Filename" "Size" "Valid" + echo "$(printf '=%*s' 70 '' | tr ' ' '=')" + for f in $backups; do + local name size valid + name=$(basename "$f") + size=$(file_size "$f") + if check_magic "$f" >/dev/null 2>&1; then + valid="${GREEN}✓${NC}" + else + valid="${RED}✗${NC}" + fi + printf "%-30s %12s %b\n" "$name" "$size" "$valid" + done +} + +# --- Main --- + +case "${1:-help}" in + backup) + cmd_backup + ;; + restore) + if [ -z "${2:-}" ]; then + error "restore requires a backup file path" + echo "" + cmd_list + exit 1 + fi + cmd_restore "$2" + ;; + verify) + target="${2:-}" + if [ -z "$target" ]; then + if [ -d "$BACKUP_DIR" ]; then + target=$(ls -t "$BACKUP_DIR"/maven-db-*.enc 2>/dev/null | head -1 || true) + fi + if [ -z "$target" ]; then + target=$(resolve_db_path) + info "no backups found; verifying live database" + fi + fi + cmd_verify "$target" + ;; + list) + cmd_list + ;; + help|--help|-h) + usage + ;; + *) + error "unknown command: $1" + usage + ;; +esac