#!/bin/bash set -euo pipefail ERROR_MSG="" SUCCESS_MSG="" RESTORE_OK=0 DA_USER="" JOB_FILE="${1:-}" if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then echo "borg-restore: missing job file" >&2 exit 1 fi JOB_BASE="$(basename "$JOB_FILE")" JOB_ID="${JOB_BASE%%.*}" PLUGIN_DIR="/usr/local/directadmin/plugins/borg-restore" 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" } format_restore_path() { local path="${1:-}" path="$(echo "$path" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" if [ -z "$path" ]; then echo "" return fi if [[ "$path" != /* ]]; then path="/$path" fi echo "$path" } append_missing_include_explanation() { if [ ! -f "$LOG_FILE" ]; then return fi if ! grep -qi "Include pattern '.*' never matched" "$LOG_FILE"; then return fi if grep -Fq "W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki" "$LOG_FILE" || grep -Fq "The specified restore path was not found in the backup" "$LOG_FILE"; then return fi local label label="$(format_restore_path "${USERRESTOREPATH:-}")" local msg if [ "${USERLANG:-}" = "en" ]; then msg="The specified restore path" else msg="W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki" fi if [ -n "$label" ]; then msg+=" (${label})" fi if [ "${USERLANG:-}" = "en" ]; then msg+=" was not found in the backup. Please try restoring from another archive that may contain those files." else msg+=". Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować." fi printf "\n%s\n" "$msg" >> "$LOG_FILE" } has_missing_include() { if [ ! -f "$LOG_FILE" ]; then return 1 fi grep -qi "Include pattern '.*' never matched" "$LOG_FILE" } filter_log_content() { awk '{ line=tolower($0); if (line ~ /permanently added/) next; if (line ~ /known host/) next; print; }' } secure_source_check() { local path="$1" local label="$2" local check_writable="${3:-1}" if [ -z "$path" ] || [ ! -e "$path" ]; then fail "Missing ${label}: ${path}" exit 1 fi if [ -L "$path" ] || [ ! -f "$path" ]; then fail "Insecure ${label}: not a regular file (${path})" exit 1 fi local owner_uid owner_name mode owner_uid="$(stat -c %u "$path" 2>/dev/null || true)" owner_name="$(stat -c %U "$path" 2>/dev/null || true)" mode="$(stat -c %a "$path" 2>/dev/null || true)" if [ -z "$owner_uid" ] || [ -z "$mode" ]; then fail "Unable to stat ${label}: ${path}" exit 1 fi if [ "$owner_uid" != "0" ] && [ "$owner_uid" != "500" ] && [ "$owner_name" != "root" ] && [ "$owner_name" != "diradmin" ]; then fail "Insecure ${label} owner (${owner_name}:${owner_uid}): ${path}" exit 1 fi mode=$((10#$mode)) if [ "$check_writable" = "1" ] && (( (mode & 022) != 0 )); then fail "Insecure ${label} permissions (group/other writable): ${path}" exit 1 fi } urlencode() { local input="$1" local hex out i hex="$(printf '%s' "$input" | od -An -tx1 | tr -d ' \n')" out="" for ((i=0; i<${#hex}; i+=2)); do out+="%${hex:i:2}" done printf '%s' "$out" } notify_user() { local code="$1" if [ -z "${DA_USER:-}" ]; then return fi local subject message if [ "$code" -eq 0 ] && [ "$RESTORE_OK" -eq 1 ]; then if [ "${USERLANG:-}" = "en" ]; then subject="BorgBackup restore completed successfully" message="Restore of ${USERRESTOREPATH:-} from ${REPONAME:-} completed." else subject="Przywracanie plików BorgBackup zakończone pomyślnie" message="${SUCCESS_MSG:-Przywracanie ${USERRESTOREPATH:-} z ${REPONAME:-} zakończone.}" fi else if [ "${USERLANG:-}" = "en" ]; then subject="BorgBackup restore error" message="Restore failed for ${USERRESTOREPATH:-} from ${REPONAME:-}." else subject="Błąd przywracania plików BorgBackup" if [ -n "$ERROR_MSG" ]; then message="Błąd przywracania: ${ERROR_MSG}" else message="Błąd przywracania ${USERRESTOREPATH:-} z ${REPONAME:-}." fi fi if [ -n "${LOG_FILE:-}" ] && [ -f "$LOG_FILE" ]; then local log_content log_content="$(cat "$LOG_FILE" | filter_log_content)" 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" } on_exit() { local code="$1" notify_user "$code" } trap 'on_exit $?' EXIT secure_source_check "$JOB_FILE" "job file" 0 # shellcheck disable=SC1090 source "$JOB_FILE" DA_USER="${DA_USER:-}" if [ -z "${DA_USER:-}" ]; then echo "Missing DA_USER in job file" >&2 exit 1 fi CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf" set_owner() { local path="$1" if [ -z "$path" ] || [ ! -e "$path" ]; then return fi if [ "$RESTORETYPE" = "mail" ]; then chown -R "${DA_USER}:mail" "$path" else chown -R "${DA_USER}:${DA_USER}" "$path" fi } is_true() { local v="${1:-}" v="$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')" case "$v" in 1|true|yes|on) return 0 ;; *) return 1 ;; esac } if [ ! -f "$CONFIG_FILE" ]; then fail "Missing ${CONFIG_FILE}" exit 1 fi secure_source_check "$CONFIG_FILE" "plugin-settings.conf" # shellcheck disable=SC1090 source "$CONFIG_FILE" BB_PATH="${BB_PATH:-/opt/bb}" BORG_PATH="${BORG_PATH:-/usr/bin/borg}" BORG_VARIABLES_FILE="${BORG_VARIABLES_FILE:-}" WEB_RESTORE_LOCATION="${WEB_RESTORE_LOCATION:-/home/DA_USER/domains}" MAIL_RESTORE_LOCATION="${MAIL_RESTORE_LOCATION:-/home/DA_USER/imap}" BLOCK_RESTORE_OUTSIDE_LOCATION="${BLOCK_RESTORE_OUTSIDE_LOCATION:-TRUE}" CLEAR_DOVECOT_INDEXES_AFTER_MERGE="${CLEAR_DOVECOT_INDEXES_AFTER_MERGE:-TRUE}" RESTORE_PATH_MODE="${RESTORE_PATH_MODE:-dir}" if [ -z "$BB_PATH" ]; then fail "BB_PATH is empty in plugin-settings.conf" exit 1 fi if [ -z "$BORG_PATH" ]; then fail "BORG_PATH is empty in plugin-settings.conf" exit 1 fi BBVARS_PATH="${BORG_VARIABLES_FILE:-}" BBVARS_PATH="${BBVARS_PATH//DA_USER/${DA_USER}}" if [ -z "$BBVARS_PATH" ]; then BBVARS_PATH="$BB_PATH" if [[ "$BBVARS_PATH" != *.sh ]]; then BBVARS_PATH="${BBVARS_PATH%/}/bbvars.sh" fi fi if [ ! -f "$BBVARS_PATH" ]; then fail "Missing ${BBVARS_PATH}" exit 1 fi secure_source_check "$BBVARS_PATH" "bbvars.sh" 0 set -a # shellcheck disable=SC1091 source "$BBVARS_PATH" set +a if [ -z "${REPO:-}" ]; then fail "Missing REPO in ${BBVARS_PATH}" exit 1 fi if [ -n "${BORG_SSH_KEY:-}" ]; then if [ ! -f "${BORG_SSH_KEY}" ]; then fail "BORG_SSH_KEY is not a file: ${BORG_SSH_KEY}" exit 1 fi export BORG_RSH="ssh -i \"${BORG_SSH_KEY}\"" fi if [ -z "${REPONAME:-}" ] || [ -z "${USERRESTOREPATH:-}" ]; then fail "Missing REPONAME or USERRESTOREPATH in job file" exit 1 fi RESTOREMODE="${RESTOREMODE:-location}" RESTORETYPE="${RESTORETYPE:-web}" if [ "$RESTORETYPE" != "mail" ]; then RESTORETYPE="web" fi if [ "$RESTORETYPE" = "mail" ]; then if [ "$RESTOREMODE" != "location" ] && [ "$RESTOREMODE" != "update" ]; then RESTOREMODE="update" fi else if [ "$RESTOREMODE" != "location" ] && [ "$RESTOREMODE" != "overwrite" ]; then RESTOREMODE="location" fi fi RESTOREPATHMODE="${RESTOREPATHMODE:-$RESTORE_PATH_MODE}" RESTOREPATHMODE="$(echo "$RESTOREPATHMODE" | tr '[:upper:]' '[:lower:]')" RESTOREPATHMODE_FLAG="full" if [ "$RESTOREPATHMODE" = "dir" ]; then RESTOREPATHMODE_FLAG="dir" fi TARGETPATH="${TARGETPATH:-}" RESTORE_FULL="/${USERRESTOREPATH#/}" WEB_BASE="${WEB_RESTORE_LOCATION//DA_USER/$DA_USER}" MAIL_BASE="${MAIL_RESTORE_LOCATION//DA_USER/$DA_USER}" BASE_LOCATION="$WEB_BASE" if [ "$RESTORETYPE" = "mail" ]; then BASE_LOCATION="$MAIL_BASE" fi if [ -z "$BASE_LOCATION" ]; then fail "Base restore location is empty" exit 1 fi TARGET_DIR="$BASE_LOCATION" if [ -z "$TARGETPATH" ]; then TARGETPATH="$TARGET_DIR" fi BLOCK_FLAG="$(echo "$BLOCK_RESTORE_OUTSIDE_LOCATION" | tr '[:upper:]' '[:lower:]')" if [ "$BLOCK_FLAG" = "1" ] || [ "$BLOCK_FLAG" = "true" ] || [ "$BLOCK_FLAG" = "yes" ] || [ "$BLOCK_FLAG" = "on" ]; then if [ -n "$BASE_LOCATION" ]; then BASE_LOCATION="${BASE_LOCATION%/}" if [[ "$RESTORE_FULL" != "$BASE_LOCATION" && "$RESTORE_FULL" != "$BASE_LOCATION/"* ]]; then fail "Restore path outside base location: ${RESTORE_FULL}" exit 1 fi fi fi if [[ "$USERRESTOREPATH" == *".."* ]]; then fail "Invalid restore path (contains ..)" exit 1 fi if [[ "$TARGETPATH" == *".."* ]]; then fail "Invalid target path (contains ..)" exit 1 fi if [ "$RESTOREMODE" = "overwrite" ] || [ "$RESTOREMODE" = "update" ]; then TMP_BASE="/home/${DA_USER}/bb.tmp" RESTORE_BASE="${RESTORE_FULL##*/}" RESTORE_PARENT="${RESTORE_FULL%/*}" if [ -z "$RESTORE_BASE" ] || [ -z "$RESTORE_PARENT" ]; then fail "Nieprawidłowa ścieżka do przywrócenia." exit 1 fi mkdir -p "$TMP_BASE" chown -R "${DA_USER}:${DA_USER}" "$TMP_BASE" rm -rf "${TMP_BASE}/${USERRESTOREPATH#/}" "${TMP_BASE}/${RESTORE_BASE}" 2>/dev/null || true cd "$TMP_BASE" set +e "$BORG_PATH" extract --progress "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1 EXIT_CODE=$? set -e append_missing_include_explanation if [ "$EXIT_CODE" -ne 0 ]; then if ! has_missing_include; then fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu." fi else EXTRACTED_PATH="${TMP_BASE}/${USERRESTOREPATH#/}" TMP_ITEM="${TMP_BASE}/${RESTORE_BASE}" if [ ! -e "$EXTRACTED_PATH" ]; then fail "Nie znaleziono przywróconego katalogu: ${EXTRACTED_PATH}" EXIT_CODE=1 else if [ -e "$TMP_ITEM" ]; then rm -rf "$TMP_ITEM" 2>/dev/null || true fi mv "$EXTRACTED_PATH" "$TMP_ITEM" if [ "$RESTOREMODE" = "update" ]; then if [ -e "$RESTORE_FULL" ]; then PRE_SUFFIX="$(date +%d-%m-%Y_%H%M%S)" PRE_NAME="${RESTORE_BASE}_przed_przywroceniem_${PRE_SUFFIX}" PRE_PATH="${RESTORE_PARENT}/${PRE_NAME}" mkdir -p "$PRE_PATH" set +e rsync -a "$RESTORE_FULL"/ "$PRE_PATH"/ >> "$LOG_FILE" 2>&1 RSYNC_CODE=$? set -e if [ "$RSYNC_CODE" -ne 0 ]; then fail "Backup katalogu przed aktualizacją zakończył się błędem." EXIT_CODE=1 fi else mkdir -p "$RESTORE_FULL" fi if [ "$EXIT_CODE" -eq 0 ]; then RSYNC_EXCLUDES=() if [ "$RESTORETYPE" = "mail" ]; then RSYNC_EXCLUDES+=(--exclude 'dovecot.index*') RSYNC_EXCLUDES+=(--exclude 'dovecot-uidlist') RSYNC_EXCLUDES+=(--exclude 'dovecot-uidvalidity') RSYNC_EXCLUDES+=(--exclude 'dovecot.list.index*') RSYNC_EXCLUDES+=(--exclude 'dovecot.list.log') fi set +e rsync -a "${RSYNC_EXCLUDES[@]}" "$TMP_ITEM"/ "$RESTORE_FULL"/ >> "$LOG_FILE" 2>&1 RSYNC_CODE=$? set -e if [ "$RSYNC_CODE" -ne 0 ]; then fail "Aktualizacja danych przez rsync zakończyła się błędem." EXIT_CODE=1 else if [ "$RESTORETYPE" = "mail" ] && is_true "$CLEAR_DOVECOT_INDEXES_AFTER_MERGE"; then find "$RESTORE_FULL" -type f \( -name 'dovecot.index*' -o -name 'dovecot-uidlist' -o -name 'dovecot-uidvalidity' -o -name 'dovecot.list.index*' -o -name 'dovecot.list.log' \) -delete >> "$LOG_FILE" 2>&1 || true fi set_owner "$RESTORE_FULL" rm -rf "$TMP_BASE" RESTORE_OK=1 SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${RESTORE_FULL}." echo "$SUCCESS_MSG" >> "$LOG_FILE" fi fi else if [ -e "$RESTORE_FULL" ]; then PRE_SUFFIX="$(date +%d-%m-%Y_%H%M%S)" PRE_NAME="${RESTORE_BASE}_przed_przywroceniem_${PRE_SUFFIX}" PRE_PATH="${RESTORE_PARENT}/${PRE_NAME}" mv "$RESTORE_FULL" "$PRE_PATH" fi mkdir -p "$RESTORE_PARENT" mv "$TMP_ITEM" "$RESTORE_FULL" set_owner "$RESTORE_FULL" rm -rf "$TMP_BASE" RESTORE_OK=1 SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${RESTORE_FULL}." echo "$SUCCESS_MSG" >> "$LOG_FILE" fi fi fi else if [ ! -d "$TARGETPATH" ]; then mkdir -p "$TARGETPATH" fi if [ -d "$TARGETPATH" ] && [ "$(ls -A "$TARGETPATH" 2>/dev/null)" != "" ]; then fail "Target directory is not empty: ${TARGETPATH}" exit 1 fi set_owner "$TARGETPATH" TARGET_DIR="$TARGETPATH" if [ "$RESTOREPATHMODE_FLAG" = "dir" ]; then IFS='/' read -r -a RESTORE_PARTS <<< "${USERRESTOREPATH#/}" PARTS_COUNT="${#RESTORE_PARTS[@]}" STRIP_COUNT=0 if [ "$PARTS_COUNT" -gt 0 ]; then STRIP_COUNT=$((PARTS_COUNT - 1)) fi cd "$TARGET_DIR" set +e "$BORG_PATH" extract --progress --strip-components "$STRIP_COUNT" "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1 EXIT_CODE=$? set -e append_missing_include_explanation if [ "$EXIT_CODE" -ne 0 ]; then if ! has_missing_include; then fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu." fi else BASE_NAME="$(basename "${USERRESTOREPATH#/}")" DEST_PATH="${TARGET_DIR%/}/${BASE_NAME}" if [ -e "$DEST_PATH" ]; then set_owner "$DEST_PATH" fi RESTORE_OK=1 SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${DEST_PATH}." echo "$SUCCESS_MSG" >> "$LOG_FILE" fi else cd "$TARGET_DIR" set +e "$BORG_PATH" extract --progress "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1 EXIT_CODE=$? set -e append_missing_include_explanation set_owner "$TARGET_DIR" if [ "$EXIT_CODE" -eq 0 ]; then RESTORE_OK=1 SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${TARGET_DIR}." echo "$SUCCESS_MSG" >> "$LOG_FILE" else if ! has_missing_include; then fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu." fi fi fi fi exit "$EXIT_CODE"