Enhance backup scripts for Nextcloud, Gitea, Paperless, Vaultwarden, Immich, and VPS configurations by adding Telegram notifications upon completion. Include details such as backup size and objects backed up. Update backup documentation to reflect these changes and ensure clarity on backup processes and retention policies.
128 lines
5.7 KiB
Bash
128 lines
5.7 KiB
Bash
#!/bin/bash
|
||
# Восстановление одного файла vzdump из restic (Yandex S3) через mount.
|
||
# Не выкачивает весь репозиторий — подгружаются только нужные данные для выбранного файла.
|
||
# Запускать на хосте Proxmox под root. Требуется FUSE (restic mount).
|
||
# Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600).
|
||
#
|
||
# Использование:
|
||
# restore-one-vzdump-from-restic.sh [SNAPSHOT] [ПУТЬ_В_СНИМКЕ] [КУДА_СОХРАНИТЬ]
|
||
#
|
||
# Пример (последний снимок, CT 107, сохранить в /mnt/backup):
|
||
# ./restore-one-vzdump-from-restic.sh
|
||
# ./restore-one-vzdump-from-restic.sh latest /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-2026_02_26-02_03_14.tar.zst /mnt/backup
|
||
#
|
||
# Список снимков: restic snapshots
|
||
# Список файлов в снимке: restic ls SNAPSHOT
|
||
set -e
|
||
|
||
MOUNT_DIR="${MOUNT_DIR:-/mnt/backup/restic-mount}"
|
||
|
||
SNAPSHOT="${1:-latest}"
|
||
# Путь к файлу внутри снимка (как в restic ls) — бэкапим /mnt/backup, пути вида /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-...
|
||
FILE_IN_SNAPSHOT="${2:-}"
|
||
OUTPUT_DIR="${3:-/mnt/backup}"
|
||
|
||
if [ "$(id -u)" -ne 0 ]; then
|
||
echo "Запускайте под root."
|
||
exit 1
|
||
fi
|
||
|
||
# Секреты из Vaultwarden (объект RESTIC)
|
||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
|
||
exit 1
|
||
fi
|
||
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||
echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden."
|
||
exit 1
|
||
fi
|
||
export BW_SESSION
|
||
BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || { echo "Не удалось разблокировать Vaultwarden."; exit 1; }
|
||
RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || { echo "Не найден объект RESTIC в Vaultwarden."; exit 1; }
|
||
export RESTIC_REPOSITORY
|
||
RESTIC_REPOSITORY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value')
|
||
export AWS_ACCESS_KEY_ID
|
||
AWS_ACCESS_KEY_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value')
|
||
export AWS_SECRET_ACCESS_KEY
|
||
AWS_SECRET_ACCESS_KEY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value')
|
||
export AWS_DEFAULT_REGION
|
||
AWS_DEFAULT_REGION=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value')
|
||
[ -z "$AWS_DEFAULT_REGION" ] && AWS_DEFAULT_REGION="ru-central1"
|
||
RESTIC_PASS=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value')
|
||
for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||
if [ -z "${!var}" ]; then
|
||
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||
exit 1
|
||
fi
|
||
done
|
||
RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||
echo -n "$RESTIC_PASS" > "$RESTIC_PASSWORD_FILE"
|
||
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT INT TERM
|
||
export RESTIC_PASSWORD_FILE
|
||
|
||
if ! command -v restic >/dev/null 2>&1; then
|
||
echo "restic не установлен. Установите: apt install restic."
|
||
exit 1
|
||
fi
|
||
|
||
if [ -z "$FILE_IN_SNAPSHOT" ]; then
|
||
echo "Использование: $0 [SNAPSHOT] ПУТЬ_В_СНИМКЕ [КУДА_СОХРАНИТЬ]"
|
||
echo "Пример: $0 latest /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-2026_02_26-02_03_14.tar.zst /mnt/backup"
|
||
echo ""
|
||
echo "Доступные снимки:"
|
||
restic snapshots 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
|
||
# Resolve "latest" to snapshot ID (restic mount показывает ids/<short-id>/)
|
||
if [ "$SNAPSHOT" = "latest" ]; then
|
||
SNAPSHOT_ID=$(restic snapshots 2>/dev/null | grep -E '^[a-f0-9]{8}[[:space:]]' | tail -1 | awk '{print $1}')
|
||
else
|
||
SNAPSHOT_ID="$SNAPSHOT"
|
||
fi
|
||
[ -z "$SNAPSHOT_ID" ] && echo "Не удалось определить ID снимка." && exit 1
|
||
echo "Снимок: $SNAPSHOT_ID"
|
||
|
||
mkdir -p "$OUTPUT_DIR"
|
||
mkdir -p "$MOUNT_DIR"
|
||
|
||
# Проверяем, не смонтировано ли уже
|
||
if mountpoint -q "$MOUNT_DIR" 2>/dev/null; then
|
||
echo "Точка монтирования $MOUNT_DIR уже занята. Размонтируйте: fusermount -u $MOUNT_DIR"
|
||
exit 1
|
||
fi
|
||
|
||
echo "Монтируем репозиторий в $MOUNT_DIR ..."
|
||
restic mount "$MOUNT_DIR" &
|
||
MOUNT_PID=$!
|
||
trap 'rm -f "$RESTIC_PASSWORD_FILE"; kill $MOUNT_PID 2>/dev/null; fusermount -u "$MOUNT_DIR" 2>/dev/null; exit' EXIT INT TERM
|
||
|
||
# В смонтированном репо файлы снимка лежат в ids/<short_id>/<путь>
|
||
SOURCE_FILE="$MOUNT_DIR/ids/$SNAPSHOT_ID/$FILE_IN_SNAPSHOT"
|
||
for i in $(seq 1 45); do
|
||
if [ -e "$SOURCE_FILE" ] 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
if [ ! -e "$SOURCE_FILE" ]; then
|
||
echo "Файл не найден: $SOURCE_FILE (подождали 45 с). Проверьте: restic ls $SNAPSHOT_ID"
|
||
exit 1
|
||
fi
|
||
|
||
BASENAME_FILE="$(basename "$FILE_IN_SNAPSHOT")"
|
||
OUTPUT_FILE="$OUTPUT_DIR/$BASENAME_FILE"
|
||
echo "Копируем $SOURCE_FILE -> $OUTPUT_FILE ..."
|
||
cp "$SOURCE_FILE" "$OUTPUT_FILE"
|
||
echo "Готово: $OUTPUT_FILE ($(du -sh "$OUTPUT_FILE" | cut -f1))"
|
||
|
||
# Размонтировать
|
||
kill $MOUNT_PID 2>/dev/null || true
|
||
wait $MOUNT_PID 2>/dev/null || true
|
||
fusermount -u "$MOUNT_DIR" 2>/dev/null || true
|
||
trap - EXIT INT TERM
|
||
echo "Размонтировано. Для восстановления контейнера:"
|
||
echo " pct create NEW_VMID $OUTPUT_FILE --restore 1 --storage local-lvm"
|