150 lines
5.2 KiB
Bash
Executable file
150 lines
5.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# lilith-backup-postgres.sh
|
|
# =============================================================================
|
|
# Backs up all 16 Lilith Platform PostgreSQL databases using pg_dump --format=custom.
|
|
#
|
|
# STORAGE
|
|
# Hot: /tank/backups/lilith/postgres/daily/YYYY-MM-DD_HH/
|
|
# Retention: 7 days (pruned at end of each run)
|
|
#
|
|
# CONFIG FILE
|
|
# /var/home/lilith/.config/lilith/backup-postgres.conf
|
|
# Format — one entry per line:
|
|
# port:database:user:password
|
|
# Example:
|
|
# 25432:lilith_prod:postgres:s3cr3t
|
|
#
|
|
# RESTORE EXAMPLE
|
|
# # List available backups
|
|
# ls /tank/backups/lilith/postgres/daily/
|
|
#
|
|
# # Restore a specific database (e.g. lilith_prod from 2026-02-18_06)
|
|
# export PGPASSWORD='s3cr3t'
|
|
# pg_restore \
|
|
# --host=localhost \
|
|
# --port=25432 \
|
|
# --username=postgres \
|
|
# --dbname=lilith_prod \
|
|
# --clean \
|
|
# --if-exists \
|
|
# /tank/backups/lilith/postgres/daily/2026-02-18_06/lilith_prod_25432.dump
|
|
#
|
|
# EXIT CODE
|
|
# 0 — all databases backed up successfully
|
|
# 1 — one or more databases failed (all are still attempted)
|
|
# =============================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
readonly BACKUP_BASE="/tank/backups/lilith/postgres/daily"
|
|
readonly CONF_FILE="/var/home/lilith/.config/lilith/backup-postgres.conf"
|
|
readonly RETENTION_DAYS=7
|
|
TIMESTAMP="$(date --utc '+%Y-%m-%d_%H')"
|
|
readonly TIMESTAMP
|
|
BACKUP_DIR="${BACKUP_BASE}/${TIMESTAMP}"
|
|
readonly BACKUP_DIR
|
|
readonly LOG_PREFIX="[lilith-backup-postgres]"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
log() { echo "${LOG_PREFIX} $(date --utc '+%Y-%m-%dT%H:%M:%SZ') INFO $*"; }
|
|
warn() { echo "${LOG_PREFIX} $(date --utc '+%Y-%m-%dT%H:%M:%SZ') WARN $*" >&2; }
|
|
err() { echo "${LOG_PREFIX} $(date --utc '+%Y-%m-%dT%H:%M:%SZ') ERROR $*" >&2; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Validate prerequisites
|
|
# ---------------------------------------------------------------------------
|
|
if [[ ! -f "${CONF_FILE}" ]]; then
|
|
err "Config file not found: ${CONF_FILE}"
|
|
err "Expected format (one line per db): port:database:user:password"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v pg_dump &>/dev/null; then
|
|
err "pg_dump not found in PATH. Install postgresql-client."
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Prepare backup directory
|
|
# ---------------------------------------------------------------------------
|
|
mkdir -p "${BACKUP_DIR}"
|
|
log "Backup directory: ${BACKUP_DIR}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Trap: log any unexpected exit
|
|
# ---------------------------------------------------------------------------
|
|
trap 'err "Script exited unexpectedly at line ${LINENO}"' ERR
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Backup loop
|
|
# ---------------------------------------------------------------------------
|
|
overall_exit=0
|
|
success_count=0
|
|
fail_count=0
|
|
|
|
while IFS=: read -r port database user password || [[ -n "${port:-}" ]]; do
|
|
# Skip blank lines and comments
|
|
[[ -z "${port}" || "${port}" == \#* ]] && continue
|
|
|
|
dump_file="${BACKUP_DIR}/${database}_${port}.dump"
|
|
log "Dumping ${database} (port ${port}, user ${user}) -> ${dump_file}"
|
|
|
|
if PGPASSWORD="${password}" pg_dump \
|
|
--host=localhost \
|
|
--port="${port}" \
|
|
--username="${user}" \
|
|
--format=custom \
|
|
--compress=6 \
|
|
--no-password \
|
|
"${database}" \
|
|
> "${dump_file}" 2>&1; then
|
|
|
|
dump_size="$(du -sh "${dump_file}" | cut -f1)"
|
|
log " OK ${database} (${dump_size})"
|
|
(( success_count++ )) || true
|
|
else
|
|
err " FAIL ${database} (port ${port}) — dump failed"
|
|
# Remove incomplete file to avoid restoring corrupt data
|
|
rm -f "${dump_file}"
|
|
(( fail_count++ )) || true
|
|
overall_exit=1
|
|
fi
|
|
|
|
done < "${CONF_FILE}"
|
|
|
|
log "Completed: ${success_count} succeeded, ${fail_count} failed"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Prune old backups (keep RETENTION_DAYS worth of directories)
|
|
# ---------------------------------------------------------------------------
|
|
log "Pruning backup directories older than ${RETENTION_DAYS} days..."
|
|
prune_count=0
|
|
while IFS= read -r -d '' old_dir; do
|
|
log " Removing: ${old_dir}"
|
|
rm -rf "${old_dir}"
|
|
(( prune_count++ )) || true
|
|
done < <(find "${BACKUP_BASE}" \
|
|
-mindepth 1 \
|
|
-maxdepth 1 \
|
|
-type d \
|
|
-mtime "+${RETENTION_DAYS}" \
|
|
-print0)
|
|
|
|
log "Pruned ${prune_count} old backup directories"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Final status
|
|
# ---------------------------------------------------------------------------
|
|
if [[ "${overall_exit}" -eq 0 ]]; then
|
|
log "All databases backed up successfully"
|
|
else
|
|
err "${fail_count} database(s) failed — review logs above"
|
|
fi
|
|
|
|
exit "${overall_exit}"
|