#!/usr/bin/env bash set -euo pipefail # === METADATA === SCRIPT_NAME="$(basename "$0")" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SKILL_ROOT="$(dirname "$SCRIPT_DIR")" # === CONFIGURATION === : "${NODE_NAME:=node-a}" : "${BACKUP_LABEL:=manual}" : "${OUTPUT_DIR:=$SKILL_ROOT/outputs}" : "${BACKUP_SOURCES:=}" : "${BACKUP_EXCLUDES:=.git,node_modules,target,dist,outputs}" : "${DRY_RUN:=1}" : "${REQUIRE_CONFIRM:=1}" : "${CONFIRM_PHRASE:=I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS}" # === FUNCTIONS === log_info() { echo "[INFO] $(date -Iseconds) $*"; } log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; } die() { log_error "$@"; exit 1; } b3_file() { if command -v b3sum &>/dev/null; then b3sum "$1" | awk '{print $1}' else blake3 "$1" fi } require_confirm() { [[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0 to apply)." if [[ "$REQUIRE_CONFIRM" == "1" ]]; then echo "" echo "CONFIRMATION REQUIRED" echo "Type the phrase exactly to continue:" echo " $CONFIRM_PHRASE" read -r input [[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch; aborting." fi } json_array() { # Convert comma-separated string to JSON array local input="$1" local first=true echo -n "[" IFS=',' read -r -a items <<< "$input" for item in "${items[@]}"; do if [[ "$first" == "true" ]]; then first=false else echo -n "," fi echo -n "\"$item\"" done echo -n "]" } main() { [[ -n "$BACKUP_SOURCES" ]] || die "BACKUP_SOURCES is required (comma-separated paths)." require_confirm local ts run_id run_dir archive excludes_file manifest ts="$(date -Iseconds | tr ':' '-')" run_id="${NODE_NAME}_${BACKUP_LABEL}_${ts}" run_dir="$OUTPUT_DIR/runs/$run_id" archive="$run_dir/archive.tar.gz" excludes_file="$run_dir/excludes.txt" manifest="$run_dir/manifest.json" mkdir -p "$run_dir" # Write excludes file log_info "Writing excludes: $excludes_file" : > "$excludes_file" IFS=',' read -r -a excludes <<< "$BACKUP_EXCLUDES" for ex in "${excludes[@]}"; do echo "$ex" >> "$excludes_file" done # Build tar exclude args local tar_excludes=() while IFS= read -r pat; do [[ -n "$pat" ]] && tar_excludes+=("--exclude=$pat") done < "$excludes_file" # Expand sources local expanded_sources=() IFS=',' read -r -a sources <<< "$BACKUP_SOURCES" for src in "${sources[@]}"; do expanded_sources+=("${src/#\~/$HOME}") done # Create archive log_info "Creating archive: $archive" tar -czf "$archive" "${tar_excludes[@]}" "${expanded_sources[@]}" local archive_size archive_b3 archive_size=$(stat -c%s "$archive") archive_b3=$(b3_file "$archive") log_info "Archive size: $archive_size bytes" log_info "Archive BLAKE3: $archive_b3" # Create manifest (pure bash JSON) log_info "Writing manifest: $manifest" cat > "$manifest" < "$OUTPUT_DIR/last_run_dir.txt" log_info "Saved last run pointer: $OUTPUT_DIR/last_run_dir.txt" log_info "Backup complete." log_info "Next: ./scripts/20_encrypt_plan.sh then 21_encrypt_apply.sh" } [[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"