Skip to content
This repository was archived by the owner on Jul 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions backend/scripts/database_backup_service/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/bin/bash

BASE_DIR=$(dirname "$0")
TEMPLATE_WORKER_SCRIPT="${BASE_DIR}/lib/backup_worker.sh.template"
INSTALLED_WORKER_DIR="${HOME}/.pg_backup_manager/scripts"
CONFIG_FILE="${HOME}/.pg_backup_manager/config"
CRON_JOB_COMMENT_PREFIX="PG_AUTO_BACKUP_JOB_FOR"

ask_value() {
local prompt="$1"
local default_value="$2"
local var_name="$3"
local input_value
read -r -p "$prompt [$default_value]: " input_value
eval "$var_name=\"${input_value:-$default_value}\""
}

ask_password() {
local prompt="$1"
local var_name="$2"
local pass
echo -n "$prompt: "
stty -echo
read -r pass
stty echo
echo
eval "$var_name=\"$pass\""
}

echo "--- Установка автоматического бэкапа PostgreSQL ---"

ask_value "Имя базы данных для бэкапа" "" DB_NAME
while [ -z "$DB_NAME" ]; do echo "Имя БД не может быть пустым."; ask_value "Имя базы данных" "" DB_NAME; done

ask_value "Пользователь PostgreSQL" "$(whoami)" DB_USER
ask_value "Хост PostgreSQL (оставьте пустым для localhost)" "localhost" DB_HOST
ask_value "Порт PostgreSQL (оставьте пустым для 5432)" "5432" DB_PORT
ask_password "Пароль для пользователя ${DB_USER}" DB_PASSWORD
while [ -z "$DB_PASSWORD" ]; do echo "Пароль не может быть пустым."; ask_password "Пароль для пользователя ${DB_USER}" DB_PASSWORD; done

DEFAULT_BACKUP_DIR="${HOME}/pg_backups/${DB_NAME}"
ask_value "Директория для хранения бэкапов" "$DEFAULT_BACKUP_DIR" BACKUP_DIR
ask_value "Количество дней хранения бэкапов (целое число)" "7" DAYS_TO_KEEP
while ! [[ "$DAYS_TO_KEEP" =~ ^[0-9]+$ ]] || [ "$DAYS_TO_KEEP" -lt 1 ]; do
echo "Некорректное значение. Введите целое положительное число."
ask_value "Количество дней хранения бэкапов" "7" DAYS_TO_KEEP
done

ask_value "Минута для запуска cron (0-59)" "30" CRON_MINUTE
while ! [[ "$CRON_MINUTE" =~ ^[0-5]?[0-9]$ ]]; do echo "Неверное значение для минут."; ask_value "Минута (0-59)" "30" CRON_MINUTE; done
ask_value "Час для запуска cron (0-23)" "2" CRON_HOUR
while ! [[ "$CRON_HOUR" =~ ^([0-1]?[0-9]|2[0-3])$ ]]; do echo "Неверное значение для часов."; ask_value "Час (0-23)" "2" CRON_HOUR; done

mkdir -p "$INSTALLED_WORKER_DIR"
mkdir -p "$BACKUP_DIR"
mkdir -p "$(dirname "$CONFIG_FILE")"

PGPASS_FILE="${HOME}/.pgpass"
EFFECTIVE_DB_HOST=${DB_HOST:-localhost}
EFFECTIVE_DB_PORT=${DB_PORT:-5432}
PGPASS_ENTRY="${EFFECTIVE_DB_HOST}:${EFFECTIVE_DB_PORT}:${DB_NAME}:${DB_USER}:${DB_PASSWORD}"
PGPASS_ENTRY_CHECK="${EFFECTIVE_DB_HOST}:${EFFECTIVE_DB_PORT}:${DB_NAME}:${DB_USER}:" # Для проверки без пароля

echo "Настройка $PGPASS_FILE..."
if [ -f "$PGPASS_FILE" ]; then
if grep -qF "$PGPASS_ENTRY_CHECK" "$PGPASS_FILE"; then
echo "Похожая запись уже существует в $PGPASS_FILE."
read -r -p "Хотите обновить её с новым паролем? (y/N): " update_pgpass
if [[ "$update_pgpass" =~ ^[Yy]$ ]]; then
grep -vF "$PGPASS_ENTRY_CHECK" "$PGPASS_FILE" > "${PGPASS_FILE}.tmp" && mv "${PGPASS_FILE}.tmp" "$PGPASS_FILE"
echo "$PGPASS_ENTRY" >> "$PGPASS_FILE"
echo "Запись в $PGPASS_FILE обновлена."
else
echo "Запись в $PGPASS_FILE не изменена. Убедитесь, что существующий пароль корректен."
fi
else
echo "$PGPASS_ENTRY" >> "$PGPASS_FILE"
echo "Запись добавлена в $PGPASS_FILE."
fi
else
echo "$PGPASS_ENTRY" > "$PGPASS_FILE"
echo "$PGPASS_FILE создан с новой записью."
fi
chmod 600 "$PGPASS_FILE"
echo "Установлены права 600 на $PGPASS_FILE."

SAFE_DB_NAME_FOR_SCRIPT=$(echo "${DB_NAME}" | tr -cd '[:alnum:]_-')
SAFE_HOST_FOR_SCRIPT=$(echo "${EFFECTIVE_DB_HOST}" | tr -cd '[:alnum:]_-')
INSTALLED_WORKER_SCRIPT_NAME="backup_worker_${SAFE_DB_NAME_FOR_SCRIPT}_on_${SAFE_HOST_FOR_SCRIPT}.sh"
INSTALLED_WORKER_SCRIPT_PATH="${INSTALLED_WORKER_DIR}/${INSTALLED_WORKER_SCRIPT_NAME}"

echo "Генерация скрипта бэкапа: $INSTALLED_WORKER_SCRIPT_PATH"
sed -e "s|__DB_NAME__|${DB_NAME}|g" \
-e "s|__DB_USER__|${DB_USER}|g" \
-e "s|__DB_HOST__|${EFFECTIVE_DB_HOST}|g" \
-e "s|__DB_PORT__|${EFFECTIVE_DB_PORT}|g" \
-e "s|__BACKUP_DIR__|${BACKUP_DIR}|g" \
-e "s|__DAYS_TO_KEEP__|${DAYS_TO_KEEP}|g" \
"$TEMPLATE_WORKER_SCRIPT" > "$INSTALLED_WORKER_SCRIPT_PATH"

if [ $? -ne 0 ]; then
echo "ОШИБКА: Не удалось создать скрипт $INSTALLED_WORKER_SCRIPT_PATH"
exit 1
fi
chmod +x "$INSTALLED_WORKER_SCRIPT_PATH"
echo "Скрипт бэкапа создан и сделан исполняемым."

CRON_JOB_COMMENT="${CRON_JOB_COMMENT_PREFIX}_${SAFE_DB_NAME_FOR_SCRIPT}_ON_${SAFE_HOST_FOR_SCRIPT}"
CRON_JOB_LINE="${CRON_MINUTE} ${CRON_HOUR} * * * /bin/bash ${INSTALLED_WORKER_SCRIPT_PATH} ${CRON_JOB_COMMENT}"

echo "Добавление задачи в crontab..."
(crontab -l 2>/dev/null | grep -vF "$CRON_JOB_COMMENT" ; echo "$CRON_JOB_LINE") | crontab -
if [ $? -eq 0 ]; then
echo "Задача успешно добавлена/обновлена в crontab."
echo "Бэкап будет выполняться ежедневно в ${CRON_HOUR}:${CRON_MINUTE}."
else
echo "ОШИБКА при добавлении задачи в crontab. Попробуйте добавить вручную:"
echo "$CRON_JOB_LINE"
fi

CONFIG_KEY_PREFIX="${SAFE_DB_NAME_FOR_SCRIPT}_ON_${SAFE_HOST_FOR_SCRIPT}"
{
echo "${CONFIG_KEY_PREFIX}_WORKER_SCRIPT_PATH=\"${INSTALLED_WORKER_SCRIPT_PATH}\""
echo "${CONFIG_KEY_PREFIX}_BACKUP_DIR=\"${BACKUP_DIR}\""
echo "${CONFIG_KEY_PREFIX}_CRON_COMMENT=\"${CRON_JOB_COMMENT}\""
echo "${CONFIG_KEY_PREFIX}_DB_NAME=\"${DB_NAME}\""
echo "${CONFIG_KEY_PREFIX}_DB_USER=\"${DB_USER}\""
echo "${CONFIG_KEY_PREFIX}_DB_HOST=\"${EFFECTIVE_DB_HOST}\""
echo "${CONFIG_KEY_PREFIX}_DB_PORT=\"${EFFECTIVE_DB_PORT}\""
} >> "$CONFIG_FILE"
chmod 600 "$CONFIG_FILE"

echo "--- Установка завершена! ---"
echo "Проверьте работу, запустив: ${INSTALLED_WORKER_SCRIPT_PATH}"
echo "Логи будут в ${BACKUP_DIR}/_backup.log"
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/bash

DB_NAME="__DB_NAME__"
DB_USER="__DB_USER__"
DB_HOST="__DB_HOST__"
DB_PORT="__DB_PORT__"

BACKUP_DIR="__BACKUP_DIR__"
DAYS_TO_KEEP="__DAYS_TO_KEEP__"

set -e
set -o pipefail

LOG_FILE="${BACKUP_DIR}/_backup.log"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
SAFE_DB_NAME=$(echo "${DB_NAME}" | tr -cd '[:alnum:]._-')
FILENAME="${SAFE_DB_NAME}_${TIMESTAMP}.dump"
BACKUP_FILE_FULL_PATH="${BACKUP_DIR}/${FILENAME}"
COMPRESSED_BACKUP_FILE="${BACKUP_FILE_FULL_PATH}.gz"

PG_DUMP_OPTIONS="-Fc -Z0"

CONNECT_OPTS=""
if [ -n "$DB_HOST" ] && [ "$DB_HOST" != "localhost" ] && [ "$DB_HOST" != "127.0.0.1" ]; then
CONNECT_OPTS="$CONNECT_OPTS -h $DB_HOST"
fi
if [ -n "$DB_PORT" ] && [ "$DB_PORT" != "5432" ]; then
CONNECT_OPTS="$CONNECT_OPTS -p $DB_PORT"
fi

log_msg() {
echo "$(date +'%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}

log_msg "--------------------------------------------"
log_msg "Начало бэкапа для БД: ${DB_NAME}"
log_msg "Пользователь: ${DB_USER}, Хост: ${DB_HOST:-localhost}, Порт: ${DB_PORT:-5432}"
log_msg "Директория бэкапов: ${BACKUP_DIR}, Хранить дней: ${DAYS_TO_KEEP}"

mkdir -p "$BACKUP_DIR"
if [ ! -d "$BACKUP_DIR" ]; then
log_msg "ОШИБКА: Не удалось создать директорию ${BACKUP_DIR}"
exit 1
fi

log_msg "Создание дампа: ${BACKUP_FILE_FULL_PATH}"
pg_dump $PG_DUMP_OPTIONS $CONNECT_OPTS -U "$DB_USER" -d "$DB_NAME" -w -f "$BACKUP_FILE_FULL_PATH"

if [ $? -ne 0 ]; then
log_msg "ОШИБКА при создании дампа ${DB_NAME}!"
rm -f "$BACKUP_FILE_FULL_PATH" 2>/dev/null
exit 1
fi
log_msg "Дамп успешно создан."

log_msg "Сжатие дампа: ${COMPRESSED_BACKUP_FILE}"
gzip "$BACKUP_FILE_FULL_PATH"
if [ $? -ne 0 ]; then
log_msg "ОШИБКА при сжатии файла ${BACKUP_FILE_FULL_PATH}!"
exit 1
fi
log_msg "Дамп успешно сжат."

log_msg "Удаление старых бэкапов (старше ${DAYS_TO_KEEP} дней)..."
find "$BACKUP_DIR" -maxdepth 1 -type f -name "${SAFE_DB_NAME}_*.dump.gz" -mtime "+${DAYS_TO_KEEP}" -print -delete | while IFS= read -r line; do log_msg "Удален старый бэкап: $line"; done

log_msg "Бэкап успешно завершен."
log_msg "--------------------------------------------"
exit 0
150 changes: 150 additions & 0 deletions backend/scripts/database_backup_service/restore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/bin/bash

CONFIG_FILE="${HOME}/.pg_backup_manager/config"

ask_value() {
local prompt="$1"
local default_value="$2"
local var_name="$3"
local input_value
read -r -p "$prompt [$default_value]: " input_value
eval "$var_name=\"${input_value:-$default_value}\""
}

echo "--- Восстановление бэкапа PostgreSQL ---"

BACKUP_DIR_TO_RESTORE=""
DEFAULT_DB_NAME=""
DEFAULT_DB_USER=""
DEFAULT_DB_HOST="localhost"
DEFAULT_DB_PORT="5432"

if [ -f "$CONFIG_FILE" ]; then
echo "Найдены следующие настроенные конфигурации бэкапов:"

declare -A backup_configs
config_idx=1
while IFS= read -r line; do
if [[ "$line" =~ ^([^_]+_ON_[^_]+)_DB_NAME=\"(.*)\"$ ]]; then
prefix="${BASH_REMATCH[1]}"
db_name_display="${BASH_REMATCH[2]}"
backup_dir_display=$(grep "^${prefix}_BACKUP_DIR=" "$CONFIG_FILE" | cut -d'"' -f2)
if [ -n "$backup_dir_display" ]; then
echo "$config_idx) БД: $db_name_display (Бэкапы в: $backup_dir_display)"
backup_configs[$config_idx]="$backup_dir_display;$db_name_display"
((config_idx++))
fi
fi
done < <(grep "_DB_NAME=" "$CONFIG_FILE" | sort -u)


echo "$config_idx) Ввести путь к директории бэкапов вручную"
echo "0) Отмена"

read -r -p "Выберите конфигурацию для восстановления или введите путь вручную (0 для отмены): " choice
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then echo "Неверный ввод."; exit 1; fi
if [ "$choice" -eq 0 ]; then echo "Отмена."; exit 0; fi

if [ "$choice" -lt "$config_idx" ]; then
selected_config=${backup_configs[$choice]}
BACKUP_DIR_TO_RESTORE=$(echo "$selected_config" | cut -d';' -f1)
DEFAULT_DB_NAME=$(echo "$selected_config" | cut -d';' -f2)
fi
fi

if [ -z "$BACKUP_DIR_TO_RESTORE" ]; then
ask_value "Введите полный путь к директории с бэкапами" "" BACKUP_DIR_TO_RESTORE
fi

if [ ! -d "$BACKUP_DIR_TO_RESTORE" ]; then
echo "ОШИБКА: Директория $BACKUP_DIR_TO_RESTORE не найдена."
exit 1
fi

echo -e "\nДоступные файлы бэкапов в $BACKUP_DIR_TO_RESTORE (формат .dump.gz):"
mapfile -t backup_files < <(find "$BACKUP_DIR_TO_RESTORE" -maxdepth 1 -type f -name "*.dump.gz" -printf "%T@ %p\n" | sort -nr | cut -d' ' -f2- | xargs -L1 basename)

if [ ${#backup_files[@]} -eq 0 ]; then
echo "Бэкапы не найдены в $BACKUP_DIR_TO_RESTORE."
exit 1
fi

for i in "${!backup_files[@]}"; do
printf "%3d) %s\n" $((i+1)) "${backup_files[$i]}"
done
echo " 0) Отмена"

read -r -p "Введите номер файла для восстановления: " file_choice
if ! [[ "$file_choice" =~ ^[0-9]+$ ]] || [ "$file_choice" -lt 0 ] || [ "$file_choice" -gt ${#backup_files[@]} ]; then
echo "Неверный выбор."
exit 1
fi
if [ "$file_choice" -eq 0 ]; then echo "Восстановление отменено."; exit 0; fi


SELECTED_BACKUP_GZ_BASENAME="${backup_files[$((file_choice-1))]}"
SELECTED_BACKUP_GZ_FULLPATH="${BACKUP_DIR_TO_RESTORE}/${SELECTED_BACKUP_GZ_BASENAME}"
SELECTED_BACKUP_DUMP_FULLPATH="${SELECTED_BACKUP_GZ_FULLPATH%.gz}" # Убираем .gz

echo "Выбран файл: $SELECTED_BACKUP_GZ_FULLPATH"

echo -e "\nВведите данные для подключения к ЦЕЛЕВОЙ базе данных PostgreSQL (КУДА восстанавливать):"
ask_value "Имя целевой базы данных" "$DEFAULT_DB_NAME" RESTORE_DB_NAME
while [ -z "$RESTORE_DB_NAME" ]; do echo "Имя БД не может быть пустым."; ask_value "Имя целевой базы данных" "$DEFAULT_DB_NAME" RESTORE_DB_NAME; done

ask_value "Пользователь PostgreSQL для восстановления" "$(whoami)" RESTORE_DB_USER
ask_value "Хост PostgreSQL (оставьте пустым для localhost)" "$DEFAULT_DB_HOST" RESTORE_DB_HOST
ask_value "Порт PostgreSQL (оставьте пустым для 5432)" "$DEFAULT_DB_PORT" RESTORE_DB_PORT

CLEAN_BEFORE_RESTORE="n"
read -r -p "Очистить целевую базу данных перед восстановлением (DROP OBJECTS)? (y/N): " confirm_clean
if [[ "$confirm_clean" =~ ^[Yy]$ ]]; then
CLEAN_BEFORE_RESTORE="y"
fi

echo -e "\nВНИМАНИЕ: Это действие перезапишет данные в базе '${RESTORE_DB_NAME}' на '${RESTORE_DB_HOST:-localhost}'!"
if [[ "$CLEAN_BEFORE_RESTORE" == "y" ]]; then
echo "Все существующие объекты в базе '${RESTORE_DB_NAME}' будут УДАЛЕНЫ перед восстановлением."
fi
read -r -p "Вы абсолютно уверены, что хотите продолжить? (Введите 'YES_RESTORE' для подтверждения): " confirm_action
if [ "$confirm_action" != "YES_RESTORE" ]; then
echo "Восстановление отменено."
exit 0
fi

echo "Распаковка $SELECTED_BACKUP_GZ_FULLPATH..."
gunzip -k -f "$SELECTED_BACKUP_GZ_FULLPATH" # -k оставляет .gz, -f перезаписывает .dump если есть
if [ $? -ne 0 ]; then echo "ОШИБКА: не удалось распаковать $SELECTED_BACKUP_GZ_FULLPATH"; exit 1; fi
echo "Файл распакован: $SELECTED_BACKUP_DUMP_FULLPATH"

RESTORE_OPTS="-d $RESTORE_DB_NAME -U $RESTORE_DB_USER -v" # -v для подробного вывода
EFFECTIVE_RESTORE_DB_HOST=${RESTORE_DB_HOST:-localhost}
EFFECTIVE_RESTORE_DB_PORT=${RESTORE_DB_PORT:-5432}

if [ "$EFFECTIVE_RESTORE_DB_HOST" != "localhost" ] && [ "$EFFECTIVE_RESTORE_DB_HOST" != "127.0.0.1" ]; then
RESTORE_OPTS="$RESTORE_OPTS -h $EFFECTIVE_RESTORE_DB_HOST"
fi
if [ "$EFFECTIVE_RESTORE_DB_PORT" != "5432" ]; then
RESTORE_OPTS="$RESTORE_OPTS -p $EFFECTIVE_RESTORE_DB_PORT"
fi

if [[ "$CLEAN_BEFORE_RESTORE" == "y" ]]; then
RESTORE_OPTS="$RESTORE_OPTS --clean"
fi

echo "Запуск pg_restore..."
pg_restore $RESTORE_OPTS "$SELECTED_BACKUP_DUMP_FULLPATH"

if [ $? -eq 0 ]; then
echo "Восстановление успешно завершено."
else
echo "ОШИБКА во время восстановления. Проверьте вывод выше."
fi

read -r -p "Удалить распакованный файл ${SELECTED_BACKUP_DUMP_FULLPATH}? (y/N): " delete_dump
if [[ "$delete_dump" =~ ^[Yy]$ ]]; then
rm -f "$SELECTED_BACKUP_DUMP_FULLPATH"
echo "Файл $SELECTED_BACKUP_DUMP_FULLPATH удален."
fi

echo "--- Процесс восстановления завершен ---"
Loading