From 7875ef0bc2e02e6480d46e79a7fb36d2c1abc7ca Mon Sep 17 00:00:00 2001 From: jschantli Date: Fri, 4 Sep 2026 20:42:34 -0700 Subject: [PATCH] Adopt HHF Technology stack structure for Pangolin backup and updates --- backend/backup.py | 58 +++++++++++++++++++++++++++++++-------------- backend/pangolin.py | 29 ++++++++++++++++++++--- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/backend/backup.py b/backend/backup.py index aab1376..87d795c 100644 --- a/backend/backup.py +++ b/backend/backup.py @@ -90,45 +90,67 @@ async def run_backup_task( f''' eval P_DIR="{p_dir}" P_DIR="${{P_DIR/#~/$HOME}}" + +# Auto-discovery if configured directory does not exist or is empty if [ ! -d "$P_DIR" ] || [ -z "$(ls -A "$P_DIR" 2>/dev/null)" ]; then echo "Notice: Configured path $P_DIR is empty or does not exist." echo "Searching VPS for Pangolin Docker compose stack..." DETECTED=$(docker inspect --format '{{{{index .Config.Labels "com.docker.compose.project.working_dir"}}}}' $(docker ps -aq --filter "name=pangolin" 2>/dev/null) 2>/dev/null | head -n 1) if [ -z "$DETECTED" ] || [ ! -d "$DETECTED" ]; then - for candidate in "$HOME/config" "/root/config" "$HOME/pangolin" "/root/pangolin" "$HOME" "/opt/pangolin" /home/*/pangolin /var/lib/pangolin /srv/pangolin /etc/pangolin; do + for candidate in "$HOME/config" "/root/config" "$HOME" "/root" "$HOME/pangolin" "/root/pangolin" "/opt/pangolin"; do if [ -d "$candidate" ] && [ -n "$(ls -A "$candidate" 2>/dev/null)" ]; then DETECTED="$candidate" break fi done fi - if [ -n "$DETECTED" ] && [ -d "$DETECTED" ] && [ -n "$(ls -A "$DETECTED" 2>/dev/null)" ]; then + if [ -n "$DETECTED" ] && [ -d "$DETECTED" ]; then echo "--> Auto-discovered active Pangolin directory at: $DETECTED" P_DIR="$DETECTED" - else - echo "--> Warning: Could not locate alternate Pangolin directory. Staging from $P_DIR" fi fi +# Standard HHF Pangolin stack items +BACKUP_ITEMS=("docker-compose.yml" "compose.yaml" "docker-compose.yaml" ".env" "config" "crowdsec" "traefik") + echo "--- Staging Pangolin files from $P_DIR ---" if [ -d "$P_DIR" ]; then - rsync -av --exclude="*.db" --exclude="*.db-wal" --exclude="*.db-shm" "$P_DIR/" "{remote_stage}/pangolin/" - # If compose or env file lives in parent folder (e.g. ~ when config is ~/config), include them too - PARENT_DIR="$(dirname "$P_DIR")" - for extra in docker-compose.yml compose.yaml docker-compose.yaml .env; do - if [ -f "$PARENT_DIR/$extra" ] && [ ! -f "{remote_stage}/pangolin/$extra" ]; then - echo "--- Staging companion $extra from $PARENT_DIR ---" - cp "$PARENT_DIR/$extra" "{remote_stage}/pangolin/" 2>/dev/null || true - fi - done + if [ -f "$P_DIR/docker-compose.yml" ] || [ -d "$P_DIR/config" ]; then + echo "Detected Pangolin stack root at $P_DIR" + for item in "${{BACKUP_ITEMS[@]}}"; do + if [ -e "$P_DIR/$item" ]; then + echo "Staging $item..." + mkdir -p "{remote_stage}/pangolin/$(dirname "$item")" + cp -a "$P_DIR/$item" "{remote_stage}/pangolin/$item" 2>/dev/null || true + fi + done + else + echo "Staging contents of $P_DIR into {remote_stage}/pangolin/config/..." + mkdir -p "{remote_stage}/pangolin/config" + rsync -av --exclude="*.db" --exclude="*.db-wal" --exclude="*.db-shm" "$P_DIR/" "{remote_stage}/pangolin/config/" + + # Check parent folder for companion compose and env files + PARENT_DIR="$(dirname "$P_DIR")" + for extra in docker-compose.yml compose.yaml docker-compose.yaml .env; do + if [ -f "$PARENT_DIR/$extra" ]; then + echo "--- Staging companion $extra from $PARENT_DIR ---" + cp "$PARENT_DIR/$extra" "{remote_stage}/pangolin/" 2>/dev/null || true + fi + done + fi fi echo "--- Performing zero-lock hot-backup of SQLite databases on VPS ---" -for db in $(find "$P_DIR" -name "*.db" -o -name "*.sqlite" 2>/dev/null); do - rel=$(realpath --relative-to="$P_DIR" "$db") - mkdir -p "{remote_stage}/pangolin/$(dirname "$rel")" - echo "Hot-backing up $rel..." - sqlite3 "$db" ".backup '{remote_stage}/pangolin/$rel'" 2>/dev/null || cp -v "$db" "{remote_stage}/pangolin/$rel" +# Search in P_DIR, P_DIR/config, and $(dirname P_DIR) +for search_path in "$P_DIR" "$P_DIR/config" "$(dirname "$P_DIR")"; do + if [ -d "$search_path" ]; then + for db in $(find "$search_path" -maxdepth 2 -name "*.db" -o -name "*.sqlite" 2>/dev/null); do + dbname=$(basename "$db") + echo "Hot-backing up database: $dbname (source: $db)..." + mkdir -p "{remote_stage}/pangolin/config" + sqlite3 "$db" ".backup '{remote_stage}/pangolin/config/$dbname'" 2>/dev/null || cp -v "$db" "{remote_stage}/pangolin/config/$dbname" + done + fi done # Backup any named Docker volumes attached to pangolin containers diff --git a/backend/pangolin.py b/backend/pangolin.py index 47f980e..acd0a87 100644 --- a/backend/pangolin.py +++ b/backend/pangolin.py @@ -157,16 +157,39 @@ async def trigger_pangolin_update() -> Dict[str, Any]: # Remote update script executed on the VPS remote_script = f""" +eval P_DIR="{p_dir}" +P_DIR="${{P_DIR/#~/$HOME}}" + +# Locate compose directory +if [ -f "$P_DIR/docker-compose.yml" ] || [ -f "$P_DIR/compose.yaml" ]; then + COMPOSE_DIR="$P_DIR" +elif [ -f "$(dirname "$P_DIR")/docker-compose.yml" ] || [ -f "$(dirname "$P_DIR")/compose.yaml" ]; then + COMPOSE_DIR="$(dirname "$P_DIR")" +else + COMPOSE_DIR="$P_DIR" +fi + echo "=== STEP 1: Connecting to Pangolin VPS ({vps_host}) ===" -cd "{p_dir}" || exit 1 +echo "Operating Directory: $COMPOSE_DIR" +cd "$COMPOSE_DIR" || exit 1 + +# Detect docker compose version +if docker compose version >/dev/null 2>&1; then + DC="docker compose" +elif command -v docker-compose >/dev/null 2>&1; then + DC="docker-compose" +else + echo "ERROR: Neither 'docker compose' nor 'docker-compose' found on VPS." + exit 1 +fi echo "" echo "=== STEP 2: Pulling latest images on VPS ===" -docker compose pull +$DC pull echo "" echo "=== STEP 3: Applying stack updates & recreating containers ===" -docker compose up -d --remove-orphans +$DC up -d --remove-orphans echo "" echo "=== STEP 4: Verifying running container status ==="