#!/usr/bin/env bash set -euo pipefail umask 077 ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" COMPOSE_FILE="${ROOT_DIR}/compose.production.yml" ENV_FILE="${ROOT_DIR}/.env" BACKUP_DIR="${ROOT_DIR}/backups" RETENTION_DAYS=7 SERVICE="conductor" TIMESTAMP="$(date -u +%Y-%m-%dT%H-%M-%SZ)" usage() { printf '%s\n' "Usage: $0 [--compose-file PATH] [--env-file PATH] [--backup-dir PATH] [--retention-days N]" >&2 } while [[ $# -gt 0 ]]; do case "$1" in --compose-file) COMPOSE_FILE="$2"; shift 2 ;; --env-file) ENV_FILE="$2"; shift 2 ;; --backup-dir) BACKUP_DIR="$2"; shift 2 ;; --retention-days) RETENTION_DAYS="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage; exit 1 ;; esac done [[ -f "$COMPOSE_FILE" ]] || { echo "Compose file not found: $COMPOSE_FILE" >&2; exit 1; } [[ -f "$ENV_FILE" ]] || { echo "Environment file not found: $ENV_FILE (it contains required recovery keys)." >&2; exit 1; } [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || { echo "--retention-days must be a non-negative integer." >&2; exit 1; } compose=(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE") container_id="$("${compose[@]}" ps -q "$SERVICE")" [[ -n "$container_id" ]] || { echo "Conductor container is not created." >&2; exit 1; } [[ "$(docker inspect -f '{{.State.Running}}' "$container_id")" == "true" ]] || { echo "Conductor container is not running." >&2; exit 1; } mkdir -p "$BACKUP_DIR" chmod 700 "$BACKUP_DIR" work_dir="$(mktemp -d)" cleanup() { rm -rf -- "$work_dir"; } trap cleanup EXIT mkdir -p "$work_dir/database" "$work_dir/configuration" staged="/data/.conductor-backup-${TIMESTAMP}.db" echo "[*] Creating consistent online SQLite backup..." >&2 "${compose[@]}" exec -T "$SERVICE" node dist/scripts/backupDatabase.js "$staged" >&2 docker cp "${container_id}:${staged}" "$work_dir/database/conductor.db" "${compose[@]}" exec -T "$SERVICE" node -e 'const fs=require("fs");fs.unlinkSync(process.argv[1])' "$staged" cp "$ENV_FILE" "$work_dir/configuration/.env" cp "$COMPOSE_FILE" "$work_dir/configuration/compose.production.yml" image_ref="$(docker inspect -f '{{.Config.Image}}' "$container_id")" image_id="$(docker inspect -f '{{.Image}}' "$container_id")" cat > "$work_dir/manifest.json" < SHA256SUMS) archive="${BACKUP_DIR}/conductor_${TIMESTAMP}.tar.gz" tar -czf "$archive" -C "$work_dir" database configuration manifest.json SHA256SUMS chmod 600 "$archive" sha256sum "$archive" > "${archive}.sha256" chmod 600 "${archive}.sha256" removed=0 while IFS= read -r -d '' old; do rm -f -- "$old" "${old}.sha256"; removed=$((removed + 1)); done < <(find "$BACKUP_DIR" -maxdepth 1 -name 'conductor_*.tar.gz' -mtime +"$RETENTION_DAYS" -print0) size_bytes="$(stat -c '%s' "$archive")" result="${BACKUP_DIR}/backup-result.json" cat > "$result" <&2 cat "$result"