438 lines
12 KiB
Bash
Executable File
438 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
ERROR_MSG=""
|
|
BACKUP_OK=0
|
|
DA_USER=""
|
|
LOG_FILE=""
|
|
TMP_DB_USER="tmprestore"
|
|
TMP_DB_PASS=""
|
|
TMP_DB_ACTIVE=0
|
|
SECURE_BACKUP="true"
|
|
OWN_BACKUP_USER_RESTORE="true"
|
|
|
|
JOB_FILE="${1:-}"
|
|
if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then
|
|
echo "db-manager: missing job file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
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
|
|
fi
|
|
local subject message
|
|
if [ "$code" -eq 0 ] && [ "$BACKUP_OK" -eq 1 ]; then
|
|
subject="Backup bazy danych zakonczony pomyslnie"
|
|
message="Backup bazy ${DB_NAME:-} zostal wykonany."
|
|
else
|
|
subject="Blad wykonywania backupu bazy danych"
|
|
if [ -n "$ERROR_MSG" ]; then
|
|
message="Blad backupu: ${ERROR_MSG}"
|
|
else
|
|
message="Blad backupu bazy ${DB_NAME:-}. Log: ${LOG_FILE:-brak}."
|
|
fi
|
|
if [ -n "${LOG_FILE:-}" ] && [ -f "$LOG_FILE" ]; then
|
|
local log_content
|
|
log_content="$(cat "$LOG_FILE")"
|
|
message="${message}\n\nLog:\n${log_content}"
|
|
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"
|
|
}
|
|
|
|
db_lock_path_for_db() {
|
|
local db_name="$1"
|
|
local safe
|
|
safe="$(printf '%s' "$db_name" | tr -c 'A-Za-z0-9_.-' '_')"
|
|
printf '%s/data/lock/%s.lock' "$PLUGIN_DIR" "$safe"
|
|
}
|
|
|
|
release_db_lock() {
|
|
local db_name lock_path
|
|
db_name="${DB_NAME:-}"
|
|
if [ -z "$db_name" ] || [ -z "${PLUGIN_DIR:-}" ]; then
|
|
return 0
|
|
fi
|
|
lock_path="$(db_lock_path_for_db "$db_name")"
|
|
if [ -f "$lock_path" ]; then
|
|
rm -f "$lock_path" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
on_exit() {
|
|
local code="$1"
|
|
cleanup_tmp_db_user
|
|
notify_user "$code"
|
|
}
|
|
trap 'on_exit $?' EXIT
|
|
|
|
# shellcheck disable=SC1090
|
|
source "$JOB_FILE"
|
|
|
|
DA_USER="${DA_USER:-}"
|
|
DB_NAME="${DB_NAME:-}"
|
|
DATE_DIR="${DATE_DIR:-}"
|
|
|
|
if [ -z "$DA_USER" ] || [ -z "$DB_NAME" ]; then
|
|
echo "Missing DA_USER or DB_NAME in job file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
JOB_BASE="$(basename "$JOB_FILE")"
|
|
JOB_ID="${JOB_BASE%%.*}"
|
|
|
|
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf"
|
|
LOG_DIR="${PLUGIN_DIR}/data/logs"
|
|
LOG_FILE="${LOG_DIR}/${JOB_ID}.log"
|
|
mkdir -p "$LOG_DIR"
|
|
|
|
fail() {
|
|
local msg="$1"
|
|
ERROR_MSG="$msg"
|
|
echo "$msg" >> "$LOG_FILE"
|
|
}
|
|
|
|
if [ ! -f "$CONFIG_FILE" ]; then
|
|
fail "Missing ${CONFIG_FILE}"
|
|
exit 1
|
|
fi
|
|
|
|
# shellcheck disable=SC1090
|
|
source "$CONFIG_FILE"
|
|
|
|
USER_SQL_BACKUP_DIR="${USER_SQL_BACKUP_DIR:-/home/da_user/mysql_backups/user_backups}"
|
|
SECURE_BACKUP="${SECURE_BACKUP:-true}"
|
|
OWN_BACKUP_USER_RESTORE="${OWN_BACKUP_USER_RESTORE:-true}"
|
|
SECURE_BACKUP="$(echo "$SECURE_BACKUP" | tr '[:upper:]' '[:lower:]')"
|
|
OWN_BACKUP_USER_RESTORE="$(echo "$OWN_BACKUP_USER_RESTORE" | tr '[:upper:]' '[:lower:]')"
|
|
if [ "$SECURE_BACKUP" = "1" ] || [ "$SECURE_BACKUP" = "yes" ]; then
|
|
SECURE_BACKUP="true"
|
|
fi
|
|
if [ "$OWN_BACKUP_USER_RESTORE" = "1" ] || [ "$OWN_BACKUP_USER_RESTORE" = "yes" ]; then
|
|
OWN_BACKUP_USER_RESTORE="true"
|
|
fi
|
|
|
|
apply_da_user() {
|
|
local value="$1"
|
|
value="${value//DA_USER/$DA_USER}"
|
|
value="${value//da_user/$DA_USER}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
enforce_root_backup_tree_ownership() {
|
|
local root_dir="$1"
|
|
if [ "$(id -u)" -ne 0 ] || [ -z "$root_dir" ] || [ -L "$root_dir" ]; then
|
|
return 0
|
|
fi
|
|
mkdir -p "$root_dir" 2>/dev/null || true
|
|
find "$root_dir" \( -type d -o -type f \) -exec chown root:root {} + 2>/dev/null || true
|
|
}
|
|
|
|
USER_SQL_BACKUP_DIR="$(apply_da_user "$USER_SQL_BACKUP_DIR")"
|
|
TARGET_ROOT="$USER_SQL_BACKUP_DIR"
|
|
MYSQL_BACKUPS_ROOT="$(apply_da_user "/home/da_user/mysql_backups")"
|
|
enforce_root_backup_tree_ownership "$MYSQL_BACKUPS_ROOT"
|
|
|
|
if [[ "$DB_NAME" != "${DA_USER}_"* && "$DB_NAME" != "$DA_USER" ]]; then
|
|
fail "DB_NAME does not belong to user"
|
|
exit 1
|
|
fi
|
|
|
|
build_tmp_db_user() {
|
|
local db_name="$1"
|
|
local cleaned name
|
|
cleaned="$(printf '%s' "$db_name" | tr -c 'A-Za-z0-9_' '_')"
|
|
name="dbtmp__${cleaned}"
|
|
if [ "${#name}" -gt 32 ]; then
|
|
name="${name:0:32}"
|
|
fi
|
|
if [ -z "$name" ]; then
|
|
name="tmprestore"
|
|
fi
|
|
printf '%s' "$name"
|
|
}
|
|
|
|
TMP_DB_USER="$(build_tmp_db_user "$DB_NAME")"
|
|
|
|
if [ -z "$DATE_DIR" ]; then
|
|
DATE_DIR="$(date +%d-%m-%Y_%H_%M)"
|
|
if [ -d "${TARGET_ROOT%/}/${DATE_DIR}" ]; then
|
|
DATE_DIR="$(date +%d-%m-%Y_%H_%M_%S)"
|
|
fi
|
|
fi
|
|
if [[ "$DATE_DIR" == *".."* || "$DATE_DIR" == */* ]]; then
|
|
fail "Invalid DATE_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
TARGET_DIR="${TARGET_ROOT%/}/${DATE_DIR}"
|
|
mkdir -p "$TARGET_DIR"
|
|
chmod 700 "$TARGET_DIR" || true
|
|
MARKER_FILE="${TARGET_DIR}/.db-manager-owned"
|
|
{
|
|
echo "OWNER=db-manager"
|
|
echo "DA_USER=${DA_USER}"
|
|
echo "DATE_DIR=${DATE_DIR}"
|
|
echo "CREATED_TS=$(date +%s)"
|
|
} > "$MARKER_FILE"
|
|
chmod 600 "$MARKER_FILE" 2>/dev/null || true
|
|
if [ "$(id -u)" -eq 0 ] && [ "$OWN_BACKUP_USER_RESTORE" = "true" ]; then
|
|
chown root:root "$TARGET_DIR" "$MARKER_FILE" 2>/dev/null || true
|
|
fi
|
|
|
|
TARGET_FILE="${TARGET_DIR}/${DB_NAME}.sql.gz"
|
|
if [ -e "$TARGET_FILE" ]; then
|
|
IDX=1
|
|
while [ -e "${TARGET_DIR}/${DB_NAME}_${IDX}.sql.gz" ]; do
|
|
IDX=$((IDX+1))
|
|
done
|
|
TARGET_FILE="${TARGET_DIR}/${DB_NAME}_${IDX}.sql.gz"
|
|
fi
|
|
|
|
MYSQL_CONF="/usr/local/directadmin/conf/mysql.conf"
|
|
if [ ! -f "$MYSQL_CONF" ]; then
|
|
fail "Missing ${MYSQL_CONF}"
|
|
exit 1
|
|
fi
|
|
|
|
MYSQL_USER=""
|
|
MYSQL_PASS=""
|
|
MYSQL_HOST=""
|
|
MYSQL_PORT=""
|
|
MYSQL_SOCKET=""
|
|
while IFS='=' read -r key val; do
|
|
key="${key%%[[:space:]]*}"
|
|
val="${val##[[:space:]]}"
|
|
case "$key" in
|
|
user) MYSQL_USER="$val" ;;
|
|
passwd) MYSQL_PASS="$val" ;;
|
|
host) MYSQL_HOST="$val" ;;
|
|
port) MYSQL_PORT="$val" ;;
|
|
socket) MYSQL_SOCKET="$val" ;;
|
|
esac
|
|
done < <(grep -v '^[[:space:]]*#' "$MYSQL_CONF" | grep -v '^[[:space:]]*$')
|
|
|
|
MYSQL_OPTS=()
|
|
if [ -n "$MYSQL_USER" ]; then MYSQL_OPTS+=("-u${MYSQL_USER}"); fi
|
|
if [ -n "$MYSQL_PASS" ]; then MYSQL_OPTS+=("-p${MYSQL_PASS}"); fi
|
|
if [ -n "$MYSQL_HOST" ]; then MYSQL_OPTS+=("-h${MYSQL_HOST}"); fi
|
|
if [ -n "$MYSQL_PORT" ] && [ "$MYSQL_PORT" != "0" ]; then MYSQL_OPTS+=("-P${MYSQL_PORT}"); fi
|
|
if [ -n "$MYSQL_SOCKET" ]; then MYSQL_OPTS+=("--socket=${MYSQL_SOCKET}"); fi
|
|
|
|
PATH="/usr/local/mysql/bin:/usr/local/mariadb/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
|
|
export PATH
|
|
|
|
resolve_bin() {
|
|
local name path
|
|
for name in "$@"; do
|
|
if path="$(command -v "$name" 2>/dev/null)"; then
|
|
printf '%s' "$path"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
sql_escape() {
|
|
local v="$1"
|
|
v="${v//\\/\\\\}"
|
|
v="${v//\'/\\\'}"
|
|
printf '%s' "$v"
|
|
}
|
|
|
|
random_password() {
|
|
rand_chars() {
|
|
local charset="$1"
|
|
local need="$2"
|
|
local out="" chunk
|
|
while [ "${#out}" -lt "$need" ]; do
|
|
chunk="$(LC_ALL=C tr -dc "$charset" </dev/urandom 2>/dev/null | head -c $((need - ${#out})) || true)"
|
|
if [ -z "$chunk" ]; then
|
|
break
|
|
fi
|
|
out+="$chunk"
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
local lower='abcdefghijklmnopqrstuvwxyz'
|
|
local upper='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
local digits='0123456789'
|
|
local special='-'
|
|
local all="${lower}${upper}${digits}${special}"
|
|
local seed extra mixed out tmp
|
|
local -a chars=()
|
|
local i j
|
|
|
|
seed="$(rand_chars "$lower" 1)$(rand_chars "$upper" 1)$(rand_chars "$digits" 1)$(rand_chars "$special" 1)"
|
|
extra="$(rand_chars "$all" 20)"
|
|
mixed="${seed}${extra}"
|
|
|
|
if [ "${#mixed}" -lt 24 ]; then
|
|
mixed="aA1-"
|
|
while [ "${#mixed}" -lt 24 ]; do
|
|
mixed="${mixed}aA1-"
|
|
done
|
|
mixed="${mixed:0:24}"
|
|
fi
|
|
|
|
for ((i=0; i<${#mixed}; i++)); do
|
|
chars+=("${mixed:i:1}")
|
|
done
|
|
for ((i=${#chars[@]}-1; i>0; i--)); do
|
|
j=$((RANDOM % (i + 1)))
|
|
tmp="${chars[i]}"
|
|
chars[i]="${chars[j]}"
|
|
chars[j]="$tmp"
|
|
done
|
|
|
|
out=""
|
|
for tmp in "${chars[@]}"; do
|
|
out+="$tmp"
|
|
done
|
|
printf '%s' "${out:0:24}"
|
|
}
|
|
|
|
cleanup_tmp_db_user() {
|
|
if [ "${TMP_DB_ACTIVE:-0}" -ne 1 ]; then
|
|
return
|
|
fi
|
|
if [ -z "${MYSQL_BIN:-}" ]; then
|
|
return
|
|
fi
|
|
local user_esc
|
|
user_esc="$(sql_escape "$TMP_DB_USER")"
|
|
local sql="
|
|
DROP USER IF EXISTS '${user_esc}'@'localhost';
|
|
DROP USER IF EXISTS '${user_esc}'@'127.0.0.1';
|
|
FLUSH PRIVILEGES;"
|
|
set +e
|
|
"${MYSQL_BIN}" "${MYSQL_OPTS[@]}" -e "$sql" >>"$LOG_FILE" 2>&1
|
|
set -e
|
|
TMP_DB_ACTIVE=0
|
|
}
|
|
|
|
create_tmp_db_user_for_backup() {
|
|
TMP_DB_PASS="$(random_password)"
|
|
local user_esc pass_esc db_esc sql
|
|
user_esc="$(sql_escape "$TMP_DB_USER")"
|
|
pass_esc="$(sql_escape "$TMP_DB_PASS")"
|
|
db_esc="${DB_NAME//\`/\`\`}"
|
|
sql="
|
|
DROP USER IF EXISTS '${user_esc}'@'localhost';
|
|
DROP USER IF EXISTS '${user_esc}'@'127.0.0.1';
|
|
CREATE USER '${user_esc}'@'localhost' IDENTIFIED BY '${pass_esc}';
|
|
CREATE USER '${user_esc}'@'127.0.0.1' IDENTIFIED BY '${pass_esc}';
|
|
GRANT SELECT, SHOW VIEW, EVENT, TRIGGER, LOCK TABLES ON \`${db_esc}\`.* TO '${user_esc}'@'localhost';
|
|
GRANT SELECT, SHOW VIEW, EVENT, TRIGGER, LOCK TABLES ON \`${db_esc}\`.* TO '${user_esc}'@'127.0.0.1';
|
|
FLUSH PRIVILEGES;"
|
|
set +e
|
|
"${MYSQL_BIN}" "${MYSQL_OPTS[@]}" -e "$sql" >>"$LOG_FILE" 2>&1
|
|
local rc=$?
|
|
set -e
|
|
if [ "$rc" -ne 0 ]; then
|
|
return 1
|
|
fi
|
|
TMP_DB_ACTIVE=1
|
|
return 0
|
|
}
|
|
|
|
if ! MYSQLDUMP_BIN="$(resolve_bin mysqldump mariadb-dump)"; then
|
|
fail "mysqldump client not found (checked: mysqldump, mariadb-dump; PATH=${PATH})"
|
|
exit 1
|
|
fi
|
|
if ! MYSQL_BIN="$(resolve_bin mysql mariadb)"; then
|
|
fail "mysql client not found (checked: mysql, mariadb; PATH=${PATH})"
|
|
exit 1
|
|
fi
|
|
|
|
MYSQL_ADMIN_CMD=("$MYSQL_BIN" "${MYSQL_OPTS[@]}" -N -B)
|
|
MYSQL_TABLES_CMD=("${MYSQL_ADMIN_CMD[@]}")
|
|
MYSQLDUMP_CONN_OPTS=("${MYSQL_OPTS[@]}")
|
|
if [ "$SECURE_BACKUP" = "true" ]; then
|
|
if ! create_tmp_db_user_for_backup; then
|
|
fail "Nie udalo sie utworzyc tymczasowego uzytkownika bazy dla backupu."
|
|
exit 1
|
|
fi
|
|
MYSQLDUMP_CONN_OPTS=("-u${TMP_DB_USER}" "-p${TMP_DB_PASS}")
|
|
if [ -n "$MYSQL_SOCKET" ]; then
|
|
MYSQLDUMP_CONN_OPTS+=("--socket=${MYSQL_SOCKET}" "-hlocalhost")
|
|
else
|
|
MYSQLDUMP_CONN_OPTS+=("-h127.0.0.1")
|
|
if [ -n "$MYSQL_PORT" ] && [ "$MYSQL_PORT" != "0" ]; then
|
|
MYSQLDUMP_CONN_OPTS+=("-P${MYSQL_PORT}")
|
|
fi
|
|
fi
|
|
MYSQL_TABLES_CMD=("$MYSQL_BIN" "${MYSQLDUMP_CONN_OPTS[@]}" -N -B)
|
|
fi
|
|
MYSQLDUMP_CMD=("$MYSQLDUMP_BIN" "${MYSQLDUMP_CONN_OPTS[@]}" --single-transaction --routines --events --triggers --verbose --databases "$DB_NAME")
|
|
|
|
echo "Rozpoczynam backup bazy: ${DB_NAME}" >> "$LOG_FILE"
|
|
echo "Tryb backupu: verbose" >> "$LOG_FILE"
|
|
if [ "$SECURE_BACKUP" = "true" ]; then
|
|
echo "Tryb bezpieczny backupu: ON (tymczasowy uzytkownik bazy)" >> "$LOG_FILE"
|
|
else
|
|
echo "Tryb bezpieczny backupu: OFF" >> "$LOG_FILE"
|
|
fi
|
|
DB_NAME_BT="${DB_NAME//\`/\`\`}"
|
|
set +e
|
|
TABLES_RAW="$("${MYSQL_TABLES_CMD[@]}" -e "SHOW TABLES FROM \`${DB_NAME_BT}\`" 2>>"$LOG_FILE")"
|
|
TABLES_CODE=$?
|
|
set -e
|
|
if [ "$TABLES_CODE" -eq 0 ]; then
|
|
if [ -n "$TABLES_RAW" ]; then
|
|
echo "Tabele do backupu:" >> "$LOG_FILE"
|
|
while IFS= read -r tbl; do
|
|
[ -z "$tbl" ] && continue
|
|
echo " - ${tbl}" >> "$LOG_FILE"
|
|
done <<< "$TABLES_RAW"
|
|
else
|
|
echo "Baza nie zawiera tabel." >> "$LOG_FILE"
|
|
fi
|
|
else
|
|
echo "Nie udalo sie pobrac listy tabel dla ${DB_NAME}." >> "$LOG_FILE"
|
|
fi
|
|
|
|
set +e
|
|
${MYSQLDUMP_CMD[@]} 2>>"$LOG_FILE" | gzip -c > "$TARGET_FILE"
|
|
EXIT_CODE=$?
|
|
set -e
|
|
if [ "$EXIT_CODE" -ne 0 ]; then
|
|
rm -f "$TARGET_FILE"
|
|
fail "Backup bazy ${DB_NAME} zakonczony bledem."
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$(id -u)" -eq 0 ] && [ "$OWN_BACKUP_USER_RESTORE" = "true" ]; then
|
|
chmod 700 "$TARGET_DIR" 2>/dev/null || true
|
|
chmod 600 "$TARGET_FILE" "$MARKER_FILE" 2>/dev/null || true
|
|
chown root:root "$TARGET_DIR" "$TARGET_FILE" "$MARKER_FILE" 2>/dev/null || true
|
|
else
|
|
chown "${DA_USER}:${DA_USER}" "$TARGET_FILE" 2>/dev/null || true
|
|
fi
|
|
enforce_root_backup_tree_ownership "$MYSQL_BACKUPS_ROOT"
|
|
echo "Backup zapisany: ${TARGET_FILE}" >> "$LOG_FILE"
|
|
BACKUP_OK=1
|
|
exit 0
|