#!/bin/bash # ============================================================ # Nexus Autoparts — Selective Backup (No TecDoc) # Backs up schema + all data EXCEPT vehicle_parts fitments # Includes all tenant databases # ============================================================ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERROR]${NC} $*"; } fatal() { err "$*"; exit 1; } # ─── Configuration ───────────────────────────────────────────────────────── BACKUP_DIR="${BACKUP_DIR:-${PROJECT_DIR}/backups}" RETENTION_DAYS="${RETENTION_DAYS:-21}" MASTER_DB="${MASTER_DB:-nexus_autoparts}" DB_USER="${DB_USER:-nexus}" AWS_CLI="${PROJECT_DIR}/tools/bin/aws" # Load .env if exists if [[ -f "${PROJECT_DIR}/.env" ]]; then set -a source "${PROJECT_DIR}/.env" set +a fi # Parse DB credentials from DATABASE_URL or MASTER_DB_URL DB_URL="${MASTER_DB_URL:-${DATABASE_URL:-}}" if [[ -n "$DB_URL" ]]; then # Extract components from postgresql://user:pass@host/dbname DB_HOST=$(echo "$DB_URL" | sed -n 's/.*@\([^:]*\).*/\1/p') DB_PORT=$(echo "$DB_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') [[ -z "$DB_PORT" ]] && DB_PORT=5432 fi TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_NAME="nexus_backup_${TIMESTAMP}" BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}" # ─── Pre-flight checks ───────────────────────────────────────────────────── check_prerequisites() { info "Checking prerequisites..." if ! command -v pg_dump &>/dev/null; then fatal "pg_dump not found. Install: sudo apt install postgresql-client" fi if ! command -v pg_dumpall &>/dev/null; then fatal "pg_dumpall not found. Install: sudo apt install postgresql-client" fi # Test PostgreSQL connection if ! sudo -u postgres psql -c "SELECT 1" &>/dev/null; then fatal "Cannot connect to PostgreSQL. Is it running?" fi # Create backup directory mkdir -p "$BACKUP_PATH" ok "Prerequisites passed. Backup will be saved to: ${BACKUP_PATH}" } # ─── Backup master schema (structure only) ───────────────────────────────── backup_master_schema() { info "Backing up master database schema (structure only)..." local output="${BACKUP_PATH}/01_master_schema.sql" sudo -u postgres pg_dump \ --schema-only \ --no-owner \ --no-privileges \ "$MASTER_DB" > "$output" local size=$(du -h "$output" | cut -f1) ok "Master schema: ${size}" } # ─── Backup master data (excluding vehicle_parts) ────────────────────────── backup_master_data() { info "Backing up master database data (excluding vehicle_parts)..." local output="${BACKUP_PATH}/02_master_data.sql" local tables_file="${BACKUP_PATH}/_tables_to_backup.txt" # Get list of tables EXCEPT vehicle_parts sudo -u postgres psql "$MASTER_DB" -Atc " SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'vehicle_parts' ORDER BY tablename; " > "$tables_file" local table_count=$(wc -l < "$tables_file") info "Found ${table_count} tables to backup (excluded: vehicle_parts)" # Build --table arguments local table_args="" while IFS= read -r table; do [[ -n "$table" ]] && table_args="$table_args --table=$table" done < "$tables_file" # Dump data only for selected tables sudo -u postgres pg_dump \ --data-only \ --no-owner \ --no-privileges \ $table_args \ "$MASTER_DB" > "$output" # Compress gzip -f "$output" local size=$(du -h "${output}.gz" | cut -f1) ok "Master data (no TecDoc): ${size}" rm -f "$tables_file" } # ─── Backup vehicle_parts schema only (if table exists) ──────────────────── backup_vehicle_parts_schema() { local exists=$(sudo -u postgres psql "$MASTER_DB" -Atc \ "SELECT 1 FROM pg_tables WHERE schemaname='public' AND tablename='vehicle_parts'" 2>/dev/null) if [[ "$exists" != "1" ]]; then info "vehicle_parts table does not exist — skipping (legacy step)" return 0 fi info "Backing up vehicle_parts schema only (structure, no data)..." local output="${BACKUP_PATH}/03_vehicle_parts_schema.sql" sudo -u postgres pg_dump \ --schema-only \ --no-owner \ --no-privileges \ --table=vehicle_parts \ "$MASTER_DB" > "$output" local size=$(du -h "$output" | cut -f1) ok "vehicle_parts schema: ${size}" } # ─── Backup all tenant databases ─────────────────────────────────────────── # ─── Backup WA CRM (Supabase self-hosted, docker) ───────────────────────── backup_wacrm() { local container="supabase-db" if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$container"; then info "WA CRM (supabase-db) no está corriendo — se omite" return 0 fi local output="${BACKUP_PATH}/wacrm_supabase.sql" info "Backing up WA CRM (Supabase postgres)..." if docker exec "$container" pg_dump -U postgres -d postgres --clean --if-exists > "$output" 2>/dev/null; then gzip -f "$output" ok "WA CRM: $(du -h "${output}.gz" | cut -f1)" else warn "WA CRM: pg_dump falló (se omite)" rm -f "$output" fi # Volumen de storage (archivos multimedia de los chats) — solo si existe local media_tar="${BACKUP_PATH}/wacrm_storage.tar.gz" if docker volume inspect supabase_storage >/dev/null 2>&1; then if docker run --rm -v supabase_storage:/data:ro -v "${BACKUP_PATH}":/backup alpine \ tar czf /backup/wacrm_storage.tar.gz -C /data . >/dev/null 2>&1; then ok "WA CRM storage: $(du -h "$media_tar" 2>/dev/null | cut -f1)" fi fi } backup_tenants() { info "Discovering tenant databases..." local tenants_file="${BACKUP_PATH}/_tenants.txt" sudo -u postgres psql "$MASTER_DB" -Atc " SELECT db_name FROM tenants WHERE is_active = true ORDER BY id; " > "$tenants_file" local tenant_count=$(wc -l < "$tenants_file") if [[ "$tenant_count" -eq 0 ]]; then warn "No active tenants found." rm -f "$tenants_file" return fi info "Found ${tenant_count} active tenant(s). Backing up..." while IFS= read -r db_name; do [[ -z "$db_name" ]] && continue # Sanitize filename local safe_name=$(echo "$db_name" | tr -cd 'a-z0-9_') local output="${BACKUP_PATH}/tenant_${safe_name}.sql" info " → Backing up ${db_name}..." if sudo -u postgres psql -l | grep -q "^ ${db_name} "; then sudo -u postgres pg_dump \ --no-owner \ --no-privileges \ "$db_name" > "$output" gzip -f "$output" local size=$(du -h "${output}.gz" | cut -f1) ok " → ${db_name}: ${size}" else warn " → ${db_name}: database not found, skipping" fi done < "$tenants_file" rm -f "$tenants_file" } # ─── Create manifest ─────────────────────────────────────────────────────── create_manifest() { local manifest="${BACKUP_PATH}/MANIFEST.txt" cat > "$manifest" << EOF Nexus Autoparts — Selective Backup Generated: $(date '+%Y-%m-%d %H:%M:%S') Hostname: $(hostname) CONTENTS: --------- 01_master_schema.sql — Database structure (all tables, indexes, constraints) 02_master_data.sql.gz — Data for all tables EXCEPT vehicle_parts 03_vehicle_parts_schema.sql — Structure of vehicle_parts only (no data) tenant_*.sql.gz — Full backup of each active tenant database wacrm_supabase.sql.gz — Base de datos de WA CRM (Supabase self-hosted) wacrm_storage.tar.gz — Archivos multimedia de los chats de WA CRM (si existe) RESTORE INSTRUCTIONS: --------------------- 1. Create empty database: createdb nexus_autoparts 2. Restore schema: psql nexus_autoparts < 01_master_schema.sql 3. Restore data: gunzip -c 02_master_data.sql.gz | psql nexus_autoparts 4. Restore vehicle_parts structure (empty): psql nexus_autoparts < 03_vehicle_parts_schema.sql 5. Restore tenants: createdb tenant_name gunzip -c tenant_name.sql.gz | psql tenant_name RE-IMPORT TECDOC (optional): ---------------------------- To reload vehicle_parts data later: python3 scripts/import_tecdoc.py download python3 scripts/import_tecdoc.py import EOF ok "Manifest created" } # ─── Compress final archive ──────────────────────────────────────────────── create_archive() { info "Creating final archive..." local archive="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" cd "$BACKUP_DIR" tar -czf "$archive" "$BACKUP_NAME" local archive_size=$(du -h "$archive" | cut -f1) local unpacked_size=$(du -sh "$BACKUP_PATH" | cut -f1) ok "Archive created: ${archive}" echo "" echo -e " ${BOLD}Compressed:${NC} ${archive_size}" echo -e " ${BOLD}Unpacked:${NC} ${unpacked_size}" echo "" # Remove temp directory (keep archive) rm -rf "$BACKUP_PATH" } # ─── Cleanup old backups ─────────────────────────────────────────────────── cleanup_old_backups() { info "Cleaning up backups older than ${RETENTION_DAYS} days..." local deleted=0 while IFS= read -r file; do rm -f "$file" deleted=$((deleted + 1)) done < <(find "$BACKUP_DIR" -name "nexus_backup_*.tar.gz" -mtime +$RETENTION_DAYS) # Remove empty leftovers from failed runs find "$BACKUP_DIR" -maxdepth 1 -name "nexus_backup_*" -type f -empty -delete if [[ "$deleted" -gt 0 ]]; then ok "Deleted ${deleted} old backup(s)" else info "No old backups to delete" fi } # ─── Verify archive integrity ────────────────────────────────────────────── verify_archive() { local archive="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" info "Verifying archive integrity..." if ! gzip -t "$archive"; then fatal "Archive failed integrity check: ${archive}" fi if [[ $(stat -c%s "$archive") -lt 1000000 ]]; then fatal "Archive suspiciously small (<1MB): ${archive}" fi ok "Archive integrity verified" } # ─── Upload offsite (optional, needs BACKUP_S3_BUCKET) ───────────────────── upload_offsite() { local archive="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" if [[ -z "${BACKUP_S3_BUCKET:-}" ]]; then info "BACKUP_S3_BUCKET not set. Skipping offsite upload (backups are local-only)." return 0 fi if [[ ! -x "$AWS_CLI" ]]; then warn "AWS CLI not found at ${AWS_CLI}. Skipping offsite upload." return 0 fi info "Uploading to s3://${BACKUP_S3_BUCKET}/nexus-backups/ ..." if "$AWS_CLI" s3 cp "$archive" "s3://${BACKUP_S3_BUCKET}/nexus-backups/" --storage-class STANDARD_IA; then ok "Offsite upload complete" else warn "Offsite upload failed (local backup is still valid)" fi } # ─── Main ────────────────────────────────────────────────────────────────── main() { echo "" echo -e "${BOLD}${CYAN}" echo " ========================================" echo " Nexus Autoparts — Selective Backup" echo " ========================================" echo -e "${NC}" echo "" # Cleanup FIRST so a nearly-full disk cannot turn one failed backup # into permanent retention (the backup.sh vicious cycle). cleanup_old_backups check_prerequisites backup_master_schema backup_master_data backup_vehicle_parts_schema backup_tenants backup_wacrm create_manifest create_archive verify_archive upload_offsite echo -e "${BOLD}${GREEN}" echo " Backup completed successfully!" echo -e "${NC}" echo " Location: ${BACKUP_DIR}/nexus_backup_${TIMESTAMP}.tar.gz" echo "" } main "$@"