From 5c44884c5c783830b264490119c974aebe9dae4e Mon Sep 17 00:00:00 2001 From: jschantli Date: Fri, 4 Sep 2026 20:35:00 -0700 Subject: [PATCH] Auto-discover active Pangolin directory from Docker inspect and backup Docker volumes --- backend/backup.py | 48 ++++++++++++++++++++++++++++++++++++++------- backend/pangolin.py | 35 +++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/backend/backup.py b/backend/backup.py index 66a2dff..a221f64 100644 --- a/backend/backup.py +++ b/backend/backup.py @@ -87,17 +87,51 @@ async def run_backup_task( remote_commands = [ f'echo "=== STEP 1: Creating Hot-Snapshot on Remote VPS ({vps_host}) ==="', f'mkdir -p "{remote_stage}/pangolin"', - f'echo "--- Staging Pangolin files from {p_dir} ---"', - f'if [ -d "{p_dir}" ]; then rsync -av --exclude="*.db" --exclude="*.db-wal" --exclude="*.db-shm" "{p_dir}/" "{remote_stage}/pangolin/"; fi', - f'echo "--- Performing zero-lock hot-backup of SQLite databases on VPS ---"', - f""" -for db in $(find "{p_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null); do - rel=$(realpath --relative-to="{p_dir}" "$db") + f''' +P_DIR="{p_dir}" +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 /root/pangolin /opt/pangolin /home/*/pangolin /var/lib/pangolin /srv/pangolin /etc/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 + 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 + +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/" +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" done -""" + +# Backup any named Docker volumes attached to pangolin containers +for vol in $(docker inspect --format '{{{{range .Mounts}}}}{{{{if eq .Type "volume"}}}}{{{{.Source}}}}{{{{"\\n"}}}}{{{{end}}}}{{{{end}}}}' $(docker ps -aq --filter "name=pangolin" 2>/dev/null) 2>/dev/null); do + if [ -d "$vol" ] && [ -n "$(ls -A "$vol" 2>/dev/null)" ]; then + vname=$(basename "$vol") + echo "--- Staging Docker Volume: $vname from $vol ---" + mkdir -p "{remote_stage}/pangolin/volumes/$vname" + cp -a "$vol/." "{remote_stage}/pangolin/volumes/$vname/" 2>/dev/null || true + fi +done +''' ] if scope in ["vps_config", "full"]: diff --git a/backend/pangolin.py b/backend/pangolin.py index 4dc045a..8bcf679 100644 --- a/backend/pangolin.py +++ b/backend/pangolin.py @@ -31,11 +31,28 @@ async def get_pangolin_stack_status() -> Dict[str, Any]: # Query Remote VPS for files, DB, and Docker status check_script = f""" -[ -f "{p_dir}/docker-compose.yml" ] || [ -f "{p_dir}/compose.yaml" ] && echo "COMPOSE_EXISTS" || echo "COMPOSE_NO" -[ -f "{p_dir}/config/config.yml" ] || [ -f "{p_dir}/config.yml" ] && echo "CONFIG_EXISTS" || echo "CONFIG_NO" +P_DIR="{p_dir}" +if [ ! -d "$P_DIR" ] || [ -z "$(ls -A "$P_DIR" 2>/dev/null)" ]; then + 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 /root/pangolin /opt/pangolin /home/*/pangolin /var/lib/pangolin /srv/pangolin /etc/pangolin; do + if [ -d "$candidate" ] && [ -n "$(ls -A "$candidate" 2>/dev/null)" ]; then + DETECTED="$candidate" + break + fi + done + fi + if [ -n "$DETECTED" ] && [ -d "$DETECTED" ]; then + P_DIR="$DETECTED" + fi +fi +echo "RESOLVED_DIR:$P_DIR" + +[ -f "$P_DIR/docker-compose.yml" ] || [ -f "$P_DIR/compose.yaml" ] && echo "COMPOSE_EXISTS" || echo "COMPOSE_NO" +[ -f "$P_DIR/config/config.yml" ] || [ -f "$P_DIR/config.yml" ] && echo "CONFIG_EXISTS" || echo "CONFIG_NO" # Check DB -db=$(find "{p_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null | head -n 1) +db=$(find "$P_DIR" -name "*.db" -o -name "*.sqlite" 2>/dev/null | head -n 1) if [ -n "$db" ]; then echo "DB_PATH:$db" wc -c "$db" 2>/dev/null | awk '{{print $1}}' @@ -45,7 +62,7 @@ else fi # Docker containers -docker ps -a --format '{{json .}}' 2>/dev/null || true +docker ps -a --format '{{{{json .}}}}' 2>/dev/null || true """ ret, stdout, stderr = await run_remote_quick(check_script, settings) @@ -54,7 +71,7 @@ docker ps -a --format '{{json .}}' 2>/dev/null || true "vps_configured": True, "connected": False, "installed": False, - "path": p_dir, + "path": resolved_path, "compose_exists": False, "config_exists": False, "database": {"found": False, "path": None, "size_bytes": 0}, @@ -66,6 +83,12 @@ docker ps -a --format '{{json .}}' 2>/dev/null || true } lines = stdout.splitlines() + resolved_path = p_dir + for l in lines: + if l.startswith("RESOLVED_DIR:"): + resolved_path = l.split(":", 1)[1].strip() + break + compose_exists = any("COMPOSE_EXISTS" in l for l in lines) config_exists = any("CONFIG_EXISTS" in l for l in lines) @@ -107,7 +130,7 @@ docker ps -a --format '{{json .}}' 2>/dev/null || true "vps_configured": True, "connected": True, "installed": compose_exists, - "path": p_dir, + "path": resolved_path, "compose_exists": compose_exists, "config_exists": config_exists, "database": {