#!/bin/bash set -Eeuo pipefail ERROR_MSG="" BACKUP_OK=0 DA_USER="" DB_NAME="" JOB_ID="" LOG_FILE="" TMP_DB_USER="" TMP_DB_PASS="" TMP_DB_ACTIVE=0 JOB_FILE="${1:-}" if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then echo "da-postgresql: missing job file" >&2 exit 1 fi PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf" LOG_DIR="${PLUGIN_DIR}/data/logs" mkdir -p "$LOG_DIR" urlencode() { local s="$1" local out="" local i c for ((i=0; i<${#s}; i++)); do c="${s:i:1}" case "$c" in [a-zA-Z0-9.~_-]) out+="$c" ;; ' ') out+='%20' ;; *) printf -v c '%%%02X' "'$c"; out+="$c" ;; esac done printf '%s' "$out" } notify_user() { local code="$1" if [ -z "${DA_USER:-}" ]; then return 0 fi local subject message if [ "$code" -eq 0 ] && [ "$BACKUP_OK" -eq 1 ]; then subject="Backup bazy PostgreSQL zakończony pomyślnie" message="Backup bazy ${DB_NAME} został wykonany. Plik: ${TARGET_FILE:-nieznany}." else subject="Błąd wykonywania backupu bazy PostgreSQL" if [ -n "$ERROR_MSG" ]; then message="Backup bazy ${DB_NAME} nie powiódł się: ${ERROR_MSG}" else message="Backup bazy ${DB_NAME} nie powiódł się. Sprawdź log zadania." fi fi local task_queue="/usr/local/directadmin/data/task.queue" local users="select1%3D$(urlencode "$DA_USER")" local line="action=notify&value=users&subject=$(urlencode "$subject")&message=$(urlencode "$message")&users=${users}" printf "%s\n" "$line" >> "$task_queue" } fail() { local msg="$1" ERROR_MSG="$msg" if [ -n "${LOG_FILE:-}" ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${msg}" >> "$LOG_FILE" fi } log_unexpected_error() { local line_no="$1" local command="$2" local code="$3" if [ -z "${LOG_FILE:-}" ]; then return 0 fi if [ -n "${ERROR_MSG:-}" ]; then return 0 fi ERROR_MSG="Nieoczekiwany błąd wykonywania backupu (kod ${code}, linia ${line_no})." echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG} CMD: ${command}" >> "$LOG_FILE" } load_plugin_settings() { if [ ! -f "$CONFIG_FILE" ]; then fail "Brak pliku konfiguracyjnego pluginu: $CONFIG_FILE" exit 1 fi # shellcheck disable=SC1090 source "$CONFIG_FILE" } quote_sh_value() { local value="$1" value="${value//\'/\'\\\'\'}" printf "'%s'" "$value" } set_job_var() { local key="$1" local value="$2" local tmp local owner_group="" if [ -f "$JOB_FILE" ]; then owner_group="$(stat -c '%u:%g' "$JOB_FILE" 2>/dev/null || stat -f '%u:%g' "$JOB_FILE" 2>/dev/null || true)" fi tmp="$(mktemp)" grep -v "^${key}=" "$JOB_FILE" > "$tmp" 2>/dev/null || true printf "%s=%s\n" "$key" "$(quote_sh_value "$value")" >> "$tmp" mv "$tmp" "$JOB_FILE" chmod 600 "$JOB_FILE" 2>/dev/null || true if [ "$(id -u)" -eq 0 ] && [ -n "$owner_group" ]; then chown "$owner_group" "$JOB_FILE" 2>/dev/null || true fi } load_pg_credentials() { local creds_file="/usr/local/directadmin/conf/postgresql.conf" PGHOST_VAL="" PGPORT_VAL="" PGUSER_VAL="" PGPASSWORD_VAL="" if [ ! -r "$creds_file" ]; then fail "Brak pliku poświadczeń PostgreSQL: $creds_file" exit 1 fi local line line="$(grep -Ev '^[[:space:]]*($|#)' "$creds_file" | head -n1 || true)" if [ -z "$line" ]; then fail "Pusty plik poświadczeń PostgreSQL: $creds_file" exit 1 fi if [[ "$line" == *=* ]]; then while IFS='=' read -r key value; do key="$(echo "$key" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')" value="$(echo "$value" | sed -E 's/[[:space:]]+#.*$//' | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')" case "$key" in PG_HOST) PGHOST_VAL="$value" ;; PG_PORT) PGPORT_VAL="$value" ;; PG_USER) PGUSER_VAL="$value" ;; PG_PASSWORD) PGPASSWORD_VAL="$value" ;; esac done < <(grep -Ev '^[[:space:]]*($|#)' "$creds_file") else local rest PGHOST_VAL="${line%%:*}" rest="${line#*:}" PGPORT_VAL="${rest%%:*}" rest="${rest#*:}" rest="${rest#*:}" PGUSER_VAL="${rest%%:*}" PGPASSWORD_VAL="${rest#*:}" fi [ -n "$PGHOST_VAL" ] || PGHOST_VAL="localhost" [ -n "$PGPORT_VAL" ] || PGPORT_VAL="5432" [ -n "$PGUSER_VAL" ] || PGUSER_VAL="postgres" if [ -z "$PGPASSWORD_VAL" ]; then fail "Brak hasła PostgreSQL w /usr/local/directadmin/conf/postgresql.conf" exit 1 fi } resolve_bins() { PSQL_BIN="$(command -v psql 2>/dev/null || true)" PG_DUMP_BIN="$(command -v pg_dump 2>/dev/null || true)" if [ -z "$PSQL_BIN" ] || [ -z "$PG_DUMP_BIN" ]; then fail "Brak wymaganych binariów PostgreSQL (psql/pg_dump)." exit 1 fi } sql_escape_literal() { printf '%s' "$1" | sed "s/'/''/g" } psql_admin() { local database="$1" shift PGPASSWORD="$PGPASSWORD_VAL" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$PGUSER_VAL" -d "$database" -v ON_ERROR_STOP=1 "$@" } cleanup_tmp_user() { if [ "$TMP_DB_ACTIVE" -ne 1 ] || [ -z "${TMP_DB_USER:-}" ]; then return 0 fi psql_admin postgres -c "DROP ROLE IF EXISTS \"$TMP_DB_USER\";" >/dev/null 2>&1 || true TMP_DB_ACTIVE=0 } on_exit() { local code="$1" if [ "$code" -ne 0 ] && [ -z "${ERROR_MSG:-}" ] && [ -n "${LOG_FILE:-}" ]; then ERROR_MSG="Zadanie backupu zakończyło się błędem (kod ${code}) bez zarejestrowanego szczegółu." echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG}" >> "$LOG_FILE" fi cleanup_tmp_user notify_user "$code" } trap 'log_unexpected_error "$LINENO" "$BASH_COMMAND" "$?"' ERR trap 'on_exit $?' EXIT random_tmp_password() { local value value="$(od -An -N48 -tx1 /dev/urandom 2>/dev/null | tr -d '[:space:]')" if [ -z "$value" ]; then value="$(date +%s%N)${RANDOM}${RANDOM}" fi printf '%s' "${value:0:32}" } # shellcheck disable=SC1090 source "$JOB_FILE" JOB_ID="${JOB_ID:-$(basename "$JOB_FILE" .env)}" DA_USER="${DA_USER:-}" DB_NAME="${DB_NAME:-}" DATE_DIR="${DATE_DIR:-$(date +%Y-%m-%d)}" COMPRESS_BACKUP="${COMPRESS_BACKUP:-1}" USER_BASE_BACKUP_DIR="${USER_BASE_BACKUP_DIR:-/home/${DA_USER}/postgres_backup}" JOB_COMPRESS_BACKUP="${COMPRESS_BACKUP}" JOB_USER_BASE_BACKUP_DIR="${USER_BASE_BACKUP_DIR}" LOG_FILE="${LOG_DIR}/${JOB_ID}.log" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Start backup job ${JOB_ID}" > "$LOG_FILE" if [ -z "$DA_USER" ] || [ -z "$DB_NAME" ]; then fail "Brak DA_USER lub DB_NAME w pliku zadania." exit 1 fi if [[ "$DB_NAME" != "${DA_USER}_"* ]]; then fail "Baza ${DB_NAME} nie należy do użytkownika ${DA_USER}." exit 1 fi load_plugin_settings COMPRESS_BACKUP="${JOB_COMPRESS_BACKUP:-$COMPRESS_BACKUP}" USER_BASE_BACKUP_DIR="${JOB_USER_BASE_BACKUP_DIR:-$USER_BASE_BACKUP_DIR}" load_pg_credentials resolve_bins if [[ "$COMPRESS_BACKUP" =~ ^(true|yes|on)$ ]]; then COMPRESS_BACKUP="1" fi if [[ "$COMPRESS_BACKUP" =~ ^(false|no|off)$ ]]; then COMPRESS_BACKUP="0" fi TARGET_DIR="${USER_BASE_BACKUP_DIR%/}/${DATE_DIR}" if [[ "$DATE_DIR" == *".."* || "$DATE_DIR" == */* ]]; then fail "Nieprawidłowa nazwa katalogu backupu." exit 1 fi if ! mkdir -p "$TARGET_DIR" 2>>"$LOG_FILE"; then fail "Brak dostępu do katalogu backupu: ${TARGET_DIR}. Uruchom worker jako root lub popraw uprawnienia USER_BASE_BACKUP_DIR." exit 1 fi if ! chmod 755 "$TARGET_DIR" >/dev/null 2>&1; then fail "Nie można ustawić uprawnień katalogu backupu: ${TARGET_DIR}." exit 1 fi if [ "$(id -u)" -eq 0 ] && [ -n "$DA_USER" ]; then chown "$DA_USER:$DA_USER" "$TARGET_DIR" 2>/dev/null || true fi TARGET_FILE="${TARGET_DIR}/${DB_NAME}.sql" if [ "$COMPRESS_BACKUP" = "1" ]; then TARGET_FILE="${TARGET_FILE}.gz" fi set_job_var "TARGET_FILE" "$TARGET_FILE" DB_ESC="$(sql_escape_literal "$DB_NAME")" DB_EXISTS="$(psql_admin postgres -At -c "SELECT 1 FROM pg_database WHERE datname='${DB_ESC}'" 2>/dev/null || true)" if [ -z "$DB_EXISTS" ]; then fail "Baza danych ${DB_NAME} nie istnieje." exit 1 fi TMP_DB_USER="pgtmp_bkp_$(printf '%s' "$JOB_ID" | tr -cd 'A-Za-z0-9' | tr '[:upper:]' '[:lower:]' | head -c 18)" [ -n "$TMP_DB_USER" ] || TMP_DB_USER="pgtmp_bkp_$(date +%s)" TMP_DB_PASS="$(random_tmp_password)" PASS_ESC="$(sql_escape_literal "$TMP_DB_PASS")" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tworzenie tymczasowego użytkownika backupu ${TMP_DB_USER}" >> "$LOG_FILE" psql_admin postgres -c "SET password_encryption='scram-sha-256'; DROP ROLE IF EXISTS \"$TMP_DB_USER\"; CREATE ROLE \"$TMP_DB_USER\" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION PASSWORD '${PASS_ESC}';" >> "$LOG_FILE" 2>&1 TMP_DB_ACTIVE=1 psql_admin postgres -c "GRANT CONNECT, TEMPORARY ON DATABASE \"$DB_NAME\" TO \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1 psql_admin "$DB_NAME" -c "GRANT USAGE ON SCHEMA public TO \"$TMP_DB_USER\"; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"$TMP_DB_USER\"; GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO \"$TMP_DB_USER\"; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1 echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tworzenie backupu bazy ${DB_NAME}" >> "$LOG_FILE" if [ "$COMPRESS_BACKUP" = "1" ]; then set +e PGPASSWORD="$TMP_DB_PASS" "$PG_DUMP_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$TMP_DB_USER" -d "$DB_NAME" --no-owner --no-privileges --format=plain 2>>"$LOG_FILE" | gzip -9 > "$TARGET_FILE" RC=$? set -e else set +e PGPASSWORD="$TMP_DB_PASS" "$PG_DUMP_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$TMP_DB_USER" -d "$DB_NAME" --no-owner --no-privileges --format=plain > "$TARGET_FILE" 2>>"$LOG_FILE" RC=$? set -e fi if [ "$RC" -ne 0 ]; then fail "Nie udało się utworzyć backupu bazy ${DB_NAME}." rm -f "$TARGET_FILE" >/dev/null 2>&1 || true exit 1 fi if [ "$(id -u)" -eq 0 ] && [ -n "$DA_USER" ]; then chown "$DA_USER:$DA_USER" "$TARGET_FILE" 2>/dev/null || true fi chmod 644 "$TARGET_FILE" 2>/dev/null || true echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup zapisany: ${TARGET_FILE}" >> "$LOG_FILE" CURRENT_EXT=".sql" if [ "$COMPRESS_BACKUP" = "1" ]; then CURRENT_EXT="${CURRENT_EXT}.gz" fi CURRENT_FILE="${TARGET_DIR}/${DB_NAME}-current${CURRENT_EXT}" if [ -n "$USER_BASE_BACKUP_DIR" ] && [ -d "$USER_BASE_BACKUP_DIR" ]; then find "$USER_BASE_BACKUP_DIR" -maxdepth 3 -type f -name "${DB_NAME}-current.sql*" -exec rm -f {} + >/dev/null 2>&1 || true fi ln -sfn "$TARGET_FILE" "$CURRENT_FILE" 2>/dev/null || cp -f "$TARGET_FILE" "$CURRENT_FILE" >/dev/null 2>&1 || true if [ "$(id -u)" -eq 0 ] && [ -n "$DA_USER" ]; then chown "$DA_USER:$DA_USER" "$CURRENT_FILE" 2>/dev/null || true fi chmod 644 "$CURRENT_FILE" 2>/dev/null || true BACKUP_OK=1 exit 0