- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
382 lines
13 KiB
Bash
Executable File
382 lines
13 KiB
Bash
Executable File
#!/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 "$@"
|