Страница 1 из 1

Архивирование системы нового поколения

СообщениеДобавлено: 22 ноя 2025, 14:41
ROOT
Архитектура скрипта (конспект, чтобы быстро сориентироваться)
  1. Цель
    • Унифицированный инструмент резервного копирования и восстановления Fedora-подобных систем (tar-архивы: efi, boot, rootfs, home, var).
    • Поддержка живой (live) и оффлайн (rescue) работы.
    • Простая разметка диска (MAKE_PART), проверка и автоматическое приведение /etc/fstab при восстановлении.
  2. Ключевые переменные
    • SYS_DEV — устройство «системного» диска (по умолчанию /dev/nvme0n1 в текущей конфигурации).
    • SYS_DIR — точка монтирования для оффлайн-восстановления (по умолчанию /mnt/fedora).
    • BACKUP_ROOT / BACKUP_PATH — корень и каталог конкретного бэкапа.
    • TYPEFS — опция типа при монтировании (например -t xfs).
    • COMPRESSOR — выбранный компрессор (zstd/xz) исходя из типа носителя.
  3. Основные функции
    • CHECK_BACKUP_ENV — проверяет BACKUP_ROOT, SYS_DEV, и предотвращает «самобэкап».
    • SELECT_COMPRESSOR — выбирает компрессор по устройству (rota -> HDD/SSD).
    • DETECT_PARTS_LABELS (best-effort) — ищет разделы по ярлыкам (LABEL/PARTLABEL).
    • MOUNT_SYS / UMOUNT_SYS — логика монтирования корня и вспомогательных точек; учитывает live/rescue.
    • SET_PARTS — формирует карту PARTS и порядок PARTS_ORDER (efi, boot, rootfs, home, var).
    • CREATE_TAR — создание архивов в гарантированном порядке. Поддерживает компрессоры, контроль сумм, очистку старых бэкапов.
    • RECOVERY_TAR — восстановление: распаковка архивов в целевые точки, обновление /etc/fstab, подготовка chroot, установка загрузчика (EFI: не используем grub2-install; ставим пакеты + efibootmgr запись).
    • MAKE_PART — разбивка и форматирование диска (parted + mkfs/mkswap).
    • Логирование: _log и обёртки log_info, log_warn, log_error, log_ok с записью в LOG_FILE и syslog.
  4. Ключевая логика действий
    • При backup: CHECK_BACKUP_ENV → MOUNT_SYS → SELECT_COMPRESSOR → SET_PARTS → CREATE_TAR → UMOUNT_SYS (если не live).
    • При recovery: CHECK_BACKUP_ENV → подтверждение → монтирование целевой root в SYS_DIR → монтирование boot/efi/home/var в правильном порядке → распаковка архивов (в порядке: root, boot, efi, home, var или как в коде) → обновление /etc/fstab по UUID → bind mounts (proc/sys/dev/run) и временный resolv.conf → установка пакетов grub в chroot (EFI: пакеты + efibootmgr; BIOS: grub2-install) → восстановление resolv.conf и корректное размонтирование.
  5. Особенности/решения, принятые в процессе
    • Грубая замена resolv.conf в chroot: если в ${SYS_DIR}/etc/resolv.conf была символьная ссылка, она перемещается в бэкап, создаётся простой текстовый файл с DNS-серверами — это гарантирует работу dnf в chroot.
    • grub2-install не применяется для EFI — вместо этого: dnf install/reinstall grub2-efi-x64 grub2-efi-x64-modules shim-x64 и создание записи через efibootmgr.
    • Предотвращение дублей монтирования: монтирование boot выполняется до создания boot/efi и до монтирования efi (исправлено).
    • Подавление ACL ошибок при распаковке: используем --no-acls при создании/распаковке, чтобы избежать ошибок на VFAT и т.п.
    • Гарантированный порядок архивирования: efi → boot → rootfs → home → var (важно для прав/атрибутов).
    • Создание /var/lib/os-prober/mount в chroot, чтобы grub2-mkconfig не ругался.
  6. TODO-list (кратко, приоритеты)
    • Критично (сделать/проверить перед переносом):
    • Верифицировать финальный CREATE_TAR (версия по типу FS или единая) — ты выбрал вариант по фактическому типу ФС, убедиться, что TAR_CREATE_OPTS корректно применяются для каждого типа.
    • Добавить защиту на случай неполного BACKUP_PATH (если один архив отсутствует — продолжать и явно пометить).
    • Дополнительно логировать время работы по каждому архиву (начало/конец) — это поможет в анализе долгих rootfs запусков.
    • Проверить восстановление /etc/fstab на edge-cases (пустые UUID, отсутствующие разделы) — не перезаписывать «пустыми» значениями.
    • Обработать поведение efibootmgr: дубли-entries (возможность пометить и удалять старые записи с таким же Label при желании).

Высокий приоритет (улучшения):
  1. Реализовать режим «быстрой» архивации (исключить var/home при необходимости).
  2. Поддержка мультисжатия: возможность задавать компрессор per-part (например: efi — xz маленький, rootfs — zstd -9).
  3. Добавить опцию —dry-run для CREATE_TAR/RECOVERY_TAR (что будет сделано, без выполнения).
  4. Повысить устойчивость к SIGINT: завершение операции должно аккуратно убирать tmp-файлы ${ARC}.tmp.

Средний/низкий:
  1. Интеграция с systemd timer / unit для автоматических бэкапов.
  2. Поддержка LUKS — detect + временное открытие в recovery.
  3. Поддержка btrfs snapshots (альтернативная ветка).
system-backup.sh
Код: выделить все
#!/bin/bash
############################################################
# backup-fedora.sh — резервное копирование системы
# Версия: 3.5-stable   Автор: SSERGEY + GPT
# Дата: 2025-10-22/24
#
# Что сделано:
# - выровненные заголовки
# - контрольные суммы SHA256
# - гарантированный порядок архивирования (efi -> boot -> rootfs -> home -> var)
# - отображение "RESCUE (ISOLATED)" если применимо (косметика)
# - подавление предупреждений tar про сокеты (но сокеты не архивируются)
# - компрессор: HDD zstd -T0 -6, SSD/NVMe zstd -T0 -9 (fallback xz)
# - флаг -V: если вместе с -b -> проверяет только созданный бэкап; если без -b -> проверяет все бэкапы
# - аккуратный тайминг: Время: hh:mm:ss  - hh:mm:ss   Длительность: hh:mm:ss
# - ПРОФЕССИОНАЛЬНОЕ ЛОГИРОВАНИЕ: уровни, цвета, syslog, форматирование
############################################################

set -euo pipefail

###############################
### === CONFIGURATION === ###
###############################

SYS_DEV="/dev/nvme0n1"             # системный диск (подменяй при необходимости)
SYS_DIR="/mnt/fedora"              # точка монтирования для оффлайн
BACKUP_ROOT="/mnt/BACKUP/FEDORA"   # корень хранения бэкапов
TYPEFS="-t xfs"                    # используемый тип при монтировании rootfs
DATE=$(date +%d-%m-%Y)
BACKUP_PATH="${BACKUP_ROOT}/${DATE}_$(date +%H-%M)"
ME=$(basename "$0")

# Иконки для логов / статусов
OK_ICON="V"         # Успех
WARN_ICON="!"       # Предупреждение
ERR_ICON="X"        # Ошибка
INFO_ICON=">"       # Информационные строки, действия
SKIP_ICON="…"       # Пропуск

EXCLUDES=(
  '/proc/*' '/sys/*' '/dev/*' '/run/*' '/mnt/*/*' '/media/*' '/tmp/*' '/var/tmp/*'
  './proc/*' './sys/*' './dev/*' './run/*' './mnt/*/*' './media/*' './tmp/*' './var/tmp/*'
)

TAR_CREATE_OPTS=(--xattrs --acls --selinux --one-file-system --warning=no-file-changed --warning=no-file-ignored --ignore-failed-read)
TAR_EXTRACT_OPTS=(--xattrs --acls --selinux)

# число последних бэкапов, которые храним
KEEP_COUNT=5
COMPRESSOR_DEFAULT="xz -T0 -1"

declare -A FSTYPES=(
  [efi]="vfat"
  [boot]="xfs"
  [rootfs]="xfs"
  [home]="xfs"
  [var]="xfs"
  [swap]="swap"
)

###############################
### === ПРОФЕССИОНАЛЬНОЕ ЛОГИРОВАНИЕ === ###
###############################

# Уровни логирования
LOG_LEVEL=3  # 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG
USE_SYSLOG=true
LOG_FILE="${BACKUP_ROOT}/system-backup.log"

function _log() {
    local level="$1"; shift
    local message="$*"
    local timestamp
    timestamp="$(date '+%Y-%m-%d %H:%M:%S')"

    local level_num level_tag color_reset color_tag
    case "$level" in
        ERROR) level_num=1; level_tag="${ERR_ICON} ERROR"; color_tag="\033[1;31m" ;; # red
        WARN)  level_num=2; level_tag="${WARN_ICON} WARN "; color_tag="\033[1;33m" ;; # yellow
        INFO)  level_num=3; level_tag="${INFO_ICON} INFO "; color_tag="\033[1;34m" ;; # blue
        OK)    level_num=3; level_tag="${OK_ICON} OK   ";  color_tag="\033[1;32m" ;; # green
        DEBUG) level_num=4; level_tag=" DEBUG"; color_tag="\033[0;90m" ;; # gray
        *)     level_num=3; level_tag="INFO"; color_tag="\033[0m" ;;
    esac

    color_reset="\033[0m"

    # Пишем в лог-файл без цветов
    echo "[$timestamp] [$level_tag] $message" >> "$LOG_FILE"

    # Если уровень логирования достаточный — выводим на экран (только если есть TTY)
    if (( level_num <= LOG_LEVEL )) && [[ -t 1 ]]; then
        echo -e "${color_tag}[$timestamp] [$level_tag]${color_reset} $message"
    elif (( level_num <= LOG_LEVEL )); then
        # Если нет TTY (cron и т.д.), выводим без цветов
        echo "[$timestamp] [$level_tag] $message"
    fi

    # Отправляем в syslog (опционально)
    if [[ "$USE_SYSLOG" == true ]] && (( level_num <= 3 )); then # ERROR, WARN, INFO только
        logger -t "system-backup" "[$level_tag] $message"
    fi
}

# Удобные обёртки
log_info()  { _log INFO  "$@"; }
log_warn()  { _log WARN  "$@"; }
log_error() { _log ERROR "$@"; }
log_ok()    { _log OK    "$@"; }
log_debug() { _log DEBUG "$@"; }

# Функция для логирования заголовков разделов
log_header() {
    local message="$*"
    if [[ -t 1 ]]; then
        echo -e "\n\033[1;36m======== $message ========\033[0m" | tee -a "$LOG_FILE"
    else
        echo -e "\n======== $message ========" >> "$LOG_FILE"
        echo "======== $message ========"
    fi
}

log_separator() {
    if [[ -t 1 ]]; then
        echo -e "\033[1;90m───────────────────────────────────────────────\033[0m" | tee -a "$LOG_FILE"
    else
        echo "───────────────────────────────────────────────" | tee -a "$LOG_FILE"
    fi
}

###############################
### === MODE DETECTION === ###
###############################
log_header "ЗАПУСК СИСТЕМЫ РЕЗЕРВНОГО КОПИРОВАНИЯ"

DEFAULT_TARGET=$(systemctl get-default 2>/dev/null || echo "N/A")
ACTIVE_TARGET=$(systemctl list-units --type=target --state=active --no-legend | awk '{print $1}' | tr '\n' ' ')
IS_ISOLATED=false
if echo "$ACTIVE_TARGET" | grep -q "rescue.target" && [[ "$DEFAULT_TARGET" != "rescue.target" ]]; then
  IS_ISOLATED=true
fi

case "$DEFAULT_TARGET" in
  graphical.target) MODE="ЖИВАЯ (graphical.target)" ;;
  multi-user.target) MODE="ЖИВАЯ (multi-user.target)" ;;
  rescue.target) MODE="RESCUE (загрузка в rescue.target)" ;;
  emergency.target) MODE="EMERGENCY (минимальный)" ;;
  *) MODE="ОФФЛАЙН / внешняя система" ;;
esac
[[ "$IS_ISOLATED" == "true" ]] && MODE="${MODE} (ISOLATED)"

log_info "Скрипт: $ME"
log_info "Системный диск: $SYS_DEV"
log_info "Каталог бэкапа: $BACKUP_PATH"
log_info "Режим архивации: $MODE"
log_separator

###############################
### === HELPERS / FUNCs === ###
###############################
# PARTS in guaranteed order
function SET_PARTS() {
  declare -gA PARTS=(
    [efi]="${SYS_DIR}/boot/efi"
    [boot]="${SYS_DIR}/boot"
    [rootfs]="${SYS_DIR}"
    [home]="${SYS_DIR}/home"
    [var]="${SYS_DIR}/var"
  )
  PARTS_ORDER=(efi boot rootfs home var)
}

# === Проверка окружения BACKUP и SYS_DEV ===
function CHECK_BACKUP_ENV() {
  log_header "ПРОВЕРКА BACKUP ОКРУЖЕНИЯ"

  # 1. Проверка системного устройства
  if [[ -z "${SYS_DEV:-}" ]]; then
    log_error "Переменная SYS_DEV не установлена!"
    exit 1
  fi
  if [[ ! -b "$SYS_DEV" ]]; then
    log_error "Устройство $SYS_DEV не существует или не является блочным."
    exit 1
  fi
  log_ok "Системное устройство: $SYS_DEV"

  # 2. Проверка каталога BACKUP_ROOT
  if [[ -z "${BACKUP_ROOT:-}" ]]; then
    log_error "Переменная BACKUP_ROOT не установлена!"
    exit 1
  fi
  if [[ ! -d "$BACKUP_ROOT" ]]; then
    log_error "Каталог ${BACKUP_ROOT} не найден!"
    exit 1
  fi

  # 3. Проверяем, смонтирован ли BACKUP_ROOT или его родитель
  if mountpoint -q "$BACKUP_ROOT"; then
    local backup_dev
    backup_dev=$(findmnt -n -o SOURCE "$BACKUP_ROOT" 2>/dev/null)
    log_ok "BACKUP смонтирован с устройства: ${backup_dev}"

    # 3a. Проверяем, не находится ли BACKUP на системном диске
    if [[ "$backup_dev" == "$SYS_DEV"* ]]; then
      log_error "${SYS_DEV} используется как BACKUP-диск (${BACKUP_ROOT})!"
      log_warn "Это приведёт к циклическому архивированию."
      exit 1
    fi

  # если не смонтирован сам BACKUP_ROOT, проверяем родитель
  elif mountpoint -q "$(dirname "$BACKUP_ROOT")"; then
    local parent_dev
    parent_dev=$(findmnt -n -o SOURCE "$(dirname "$BACKUP_ROOT")" 2>/dev/null)
    log_info "${BACKUP_ROOT} — подкаталог смонтированной точки $(dirname "$BACKUP_ROOT") (${parent_dev})"

    if [[ "$parent_dev" == "$SYS_DEV"* ]]; then
      log_error "${SYS_DEV} используется как BACKUP-диск (через родительскую точку $(dirname "$BACKUP_ROOT"))!"
      log_warn "Это приведёт к циклическому архивированию."
      exit 1
    fi

  else
    log_warn "${BACKUP_ROOT} не смонтирован (и родитель тоже не смонтирован)!"
  fi

  # 4. Проверяем, что BACKUP_ROOT не находится внутри SYS_DIR (например /mnt/BACKUP при SYS_DIR=/)
  if [[ -n "${SYS_DIR:-}" && "$BACKUP_ROOT" == "$SYS_DIR"* ]]; then
    log_error "BACKUP_ROOT (${BACKUP_ROOT}) находится внутри SYS_DIR (${SYS_DIR})!"
    log_warn "Это приведёт к самосохранению архива в себя."
    exit 1
  fi

  log_ok "Проверка окружения завершена успешно."
  log_separator
}

# select best compressor based on BACKUP mount device type
function SELECT_COMPRESSOR() {
  local src parent disk rota bin
  src=$(findmnt -n -o SOURCE --target "${BACKUP_ROOT}" 2>/dev/null || true)
  if [[ -z "$src" ]]; then
    COMPRESSOR="${COMPRESSOR_DEFAULT}"
    log_warn "Не удалось определить устройство BACKUP; выбран дефолт: ${COMPRESSOR}"
    return
  fi

  parent=$(lsblk -no PKNAME "$src" 2>/dev/null || basename "$(readlink -f "$src")" | sed -E 's/p?[0-9]+$//')
  [[ -z "$parent" ]] && { COMPRESSOR="${COMPRESSOR_DEFAULT}"; return; }
  disk="/dev/${parent}"
  rota=$(lsblk -dn -o ROTA "$disk" 2>/dev/null || echo "")

  if [[ "$rota" == "1" ]]; then
    if command -v zstd &>/dev/null; then
      COMPRESSOR="zstd -T0 -6"
      bin="zstd"
    else
      COMPRESSOR="xz -T0 -1"
      bin="xz"
    fi
    log_info "BACKUP HDD → компрессор: ${COMPRESSOR}"
  else
    if command -v zstd &>/dev/null; then
      COMPRESSOR="zstd -T0 -9"
      bin="zstd"
    elif command -v xz &>/dev/null; then
      COMPRESSOR="xz -T0 -1"
      bin="xz"
    else
      COMPRESSOR="${COMPRESSOR_DEFAULT}"
      bin="$(echo ${COMPRESSOR} | awk '{print $1}')"
    fi
    log_info "BACKUP SSD/NVMe → компрессор: ${COMPRESSOR}"
  fi

  # финальная проверка что выбранный бинарник существует
  if ! command -v "${bin}" &>/dev/null; then
    log_warn "Выбранный компрессор '${bin}' не найден — откат на ${COMPRESSOR_DEFAULT}"
    COMPRESSOR="${COMPRESSOR_DEFAULT}"
  fi
}


# detect partition names by labels (best-effort)
function DETECT_PARTS_LABELS() {
  ROOT_PART=$(lsblk -lno NAME,PARTLABEL | awk '/RootFS/{print "/dev/"$1}' || true)
  BOOT_PART=$(lsblk -lno NAME,PARTLABEL | awk '/Boot/{print "/dev/"$1}' || true)
  EFI_PART=$(lsblk -lno NAME,PARTLABEL | awk '/EFI/{print "/dev/"$1}' || true)
  HOME_PART=$(lsblk -lno NAME,PARTLABEL | awk '/Homes/{print "/dev/"$1}' || true)
  VAR_PART=$(lsblk -lno NAME,PARTLABEL | awk '/Var/{print "/dev/"$1}' || true)
}

# mount system partitions (works for live/rescue/offline)
function MOUNT_SYS() {
  DETECT_PARTS_LABELS

  # решаем: живой режим или оффлайн (попробуем быть максимально аккуратными)
  if mountpoint -q / && [[ "$(findmnt -n -o TARGET /)" == "/" ]]; then
    log_info "Работаем в ЖИВОЙ системе (root смонтирован)"
    LIVE_MODE=true
    SYS_DIR="/"
  else
    log_info "Работаем в ОФФЛАЙН режиме — монтируем root (${SYS_DIR})"
    local rp="${ROOT_PART:-${SYS_DEV}p3}"

    # Создаем только если каталога нет
    [[ -d "${SYS_DIR}" ]] || mkdir -p "${SYS_DIR}"

    mount ${TYPEFS} "${rp}" "${SYS_DIR}" || { log_error "Ошибка монтирования RootFS (${rp})"; exit 1; }
    LIVE_MODE=false
  fi

  # сформируем PARTS и порядок
  SET_PARTS

  # Монтируем дополнительные точки ТОЛЬКО если:
  #  - мы в оффлайн-режиме (мы сами монтировали root), или
  #  - точка ожидается, но не смонтирована (на живой системе)
  suffix=""; [[ "${SYS_DEV}" =~ (nvme|mmcblk) ]] && suffix="p"
  for key in "${PARTS_ORDER[@]}"; do
    local mount_point
    mount_point=$(realpath -m "${PARTS[$key]}")
    # normalize root rep
    [[ "$mount_point" == "//" ]] && mount_point="/"

    # не пытаемся монтировать сам корень если он уже /
    if [[ "$mount_point" == "/" ]]; then
      continue
    fi

    if mountpoint -q "${mount_point}"; then
      log_ok "${mount_point} уже смонтирован."
      continue
    fi

    # если мы в live режиме и точка не смонтирована — НЕ монтируем автоматически (опасно)
    if [[ "${LIVE_MODE}" == "true" && ! -b "${mount_point}" ]]; then
      log_warn "${mount_point} не смонтирована в живой системе — пропускаем."
      continue
    fi

    # в оффлайн: монтируем по ожидаемому устройству (если есть)
    if [[ "${LIVE_MODE}" == "false" ]]; then
      mkdir -p "${mount_point}"
      case "$key" in
        efi) part="${EFI_PART:-${SYS_DEV}${suffix}1}" ;;
        boot) part="${BOOT_PART:-${SYS_DEV}${suffix}2}" ;;
        rootfs) part="${ROOT_PART:-${SYS_DEV}${suffix}3}" ;;
        home) part="${HOME_PART:-${SYS_DEV}${suffix}4}" ;;
        var) part="${VAR_PART:-${SYS_DEV}${suffix}5}" ;;
        *) part="" ;;
      esac

      if [[ -n "${part}" && -b "${part}" ]]; then
        log_info "Монтируем ${part} → ${mount_point}"
        mount "${part}" "${mount_point}" 2>/dev/null \
          && log_ok "Успешно смонтирован ${mount_point}" \
          || log_warn "Не удалось смонтировать ${mount_point} (источник: ${part})"
      else
        log_warn "Источник для ${mount_point} не найден — пропускаем."
      fi
    fi
  done
}

# unmount (only if we mounted)
function UMOUNT_SYS() {
  if [[ "${LIVE_MODE:-false}" == "true" ]]; then
    log_warn "Живая система — автоматическое размонтирование не выполняется."
  else
    log_info "Размонтируем ${SYS_DIR}..."
    umount -R "${SYS_DIR}" 2>/dev/null || log_warn "Некоторые точки не размонтировались."
  fi
}

###############################
### === CREATE BACKUP === ###
###############################
function CREATE_TAR() {
  START_TIME=$(date +%s)

  # Создаём каталог для логов только при реальном бэкапе
  mkdir -p "${BACKUP_PATH}" 2>/dev/null || true

  CHECK_BACKUP_ENV
  MOUNT_SYS
  SET_PARTS
  SELECT_COMPRESSOR

  log_header "СОЗДАНИЕ РЕЗЕРВНЫХ АРХИВОВ"

  EXCLUDE_ARGS=()
  for e in "${EXCLUDES[@]}"; do EXCLUDE_ARGS+=(--exclude="$e"); done

  log_info "Архивирование (компрессор: ${COMPRESSOR})..."

  # Определяем реальные точки монтирования и флаги для каждой файловой системы
  declare -A REAL_PARTS=()
  declare -A TAR_OPTS=()

  for part in efi boot rootfs home var; do
      SRC="${PARTS[$part]}"

      # Получаем тип файловой системы для текущей точки монтирования
      FSTYPE=$(df -T "$SRC" | awk 'NR==2 {print $2}')

      # Назначаем соответствующие флаги для tar в зависимости от типа файловой системы
      case "$FSTYPE" in
        xfs)
          TAR_OPTS["$part"]="--xattrs --acls --selinux --one-file-system --warning=no-file-changed --warning=no-file-ignored --ignore-failed-read"
          ;;
        vfat)
          TAR_OPTS["$part"]="--xattrs --acls --one-file-system --warning=no-file-changed --warning=no-file-ignored --ignore-failed-read"
          ;;
        ext4)
          TAR_OPTS["$part"]="--xattrs --acls --selinux --one-file-system --warning=no-file-changed --warning=no-file-ignored --ignore-failed-read"
          ;;
        *)
          TAR_OPTS["$part"]="--xattrs --acls --one-file-system --warning=no-file-changed --warning=no-file-ignored --ignore-failed-read"
          ;;
      esac

      # Если раздел смонтирован, добавляем его в список
      if mountpoint -q "$SRC"; then
          REAL_PARTS["$part"]="$SRC"
      else
          SRC=$(echo "$SRC" | sed -E 's#//+#/#g')
          log_warn "${SRC} не является отдельной файловой системой — пропускаем."
      fi
  done

  # Итерация по массиву с гарантированным порядком
  for part in "${PARTS_ORDER[@]}"; do
      SRC="${REAL_PARTS[$part]:-}"

      # Пропускаем, если раздел не найден
      [[ -z "$SRC" ]] && continue

      # Нормализация путей, чтобы не было //boot и т.п.
      SRC=$(echo "$SRC" | sed -E 's#//+#/#g')

      # Красивый визуальный разделитель для логов
      log_header "АРХИВИРОВАНИЕ ${part^^}"

      # Генерация архива
      ARC="${BACKUP_PATH}/${part}.tar"
      log_info "${SRC} → ${ARC}.${COMPRESSOR%% *}"

      if [[ -d "$SRC" ]]; then
          # Выбираем правильные флаги для данного типа файловой системы
          TAR_FLAGS="${TAR_OPTS[$part]}"

          tar -cpf - $TAR_FLAGS \
              "${EXCLUDE_ARGS[@]}" -C "${SRC}" . \
          | ${COMPRESSOR} > "${ARC}.tmp" \
          && mv -f "${ARC}.tmp" "${ARC}.${COMPRESSOR%% *}" \
          || { log_error "Ошибка архивирования ${part}"; rm -f "${ARC}.tmp"; UMOUNT_SYS; exit 1; }
          log_ok "Успешно заархивирован ${part}"
      else
          log_warn "Пропуск: ${SRC} не найден."
      fi
  done

  log_header "КОНТРОЛЬНЫЕ СУММЫ SHA256"
  (cd "${BACKUP_PATH}" && sha256sum *tar.* | tee -a "$LOG_FILE" > "${BACKUP_PATH}/checksums.sha256")
  log_ok "Контрольные суммы сохранены."

  log_header "ОЧИСТКА СТАРЫХ БЭКАПОВ"
  if [[ "$KEEP_COUNT" -gt 0 ]]; then
    total=$(ls -1dt "${BACKUP_ROOT}"/*/ 2>/dev/null | wc -l)
    if (( total > KEEP_COUNT )); then
      remove=$(( total - KEEP_COUNT ))
      log_warn "Удаляем старые копии, оставляем ${KEEP_COUNT}:"
      ls -1dt "${BACKUP_ROOT}"/*/ | tail -n "$remove" | while read -r old; do
        log_info "Удаляем: ${old}"
        rm -rf "${old}"
      done
    else
      log_info "Очистка не требуется — ${total} копий ≤ ${KEEP_COUNT}"
    fi
  else
    log_warn "Очистка отключена."
  fi

  # End time / Duration
  END_TIME=$(date +%s)
  DURATION=$((END_TIME - START_TIME))
  log_info "Время: $(date -d @"$START_TIME" +%T) - $(date -d @"$END_TIME" +%T)"
  log_info " Длительность: $(printf "%02d:%02d:%02d" $((DURATION/3600)) $((DURATION%3600/60)) $((DURATION%60)))"

  UMOUNT_SYS
  log_ok "Архивирование завершено. Файлы: ${BACKUP_PATH}"
}

###############################
### === VERIFY / INFO === ###
###############################

# --- Улучшенный выбор каталога для проверки (-v)
function PROMPT_VERIFY_BACKUP_DIR() {

    log_header "ДОСТУПНЫЕ БЭКАПЫ"

    local dirs=()
    mapfile -t dirs < <(ls -1dt "${BACKUP_ROOT}"/*/ 2>/dev/null)

    if (( ${#dirs[@]} == 0 )); then
        log_error "Бэкапы не найдены."
        return 1
    fi

    echo ""
    echo "Имеющиеся каталоги:"

    for d in "${dirs[@]}"; do
        printf "  %s\n" "$(basename "$d")"
    done

    echo ""
    read -r -p "Введите имя каталога (пример: 10-11-2025_04-42): " USER_DIR

    local chosen="${BACKUP_ROOT}/${USER_DIR}"

    if [[ ! -d "$chosen" ]]; then
        log_error "Каталог '${USER_DIR}' не найден."
        return 1
    fi

    echo -e "\n\033[1;36mВыбран каталог: ${USER_DIR}\033[0m\n"

    VERIFY_BACKUP_DATE "$chosen"
}

# Verify single date backup (checksums.sha256 inside that directory)
function VERIFY_BACKUP_DATE() {
  local date_dir="$1"
  [[ -z "$date_dir" ]] && return 1
  if [[ ! -f "${date_dir}/checksums.sha256" ]]; then
    log_warn "В директории ${date_dir} нет checksums.sha256"
    return 1
  fi
  log_header "ПРОВЕРКА БЭКАПА: ${date_dir}"
  (cd "${date_dir}" && sha256sum -c checksums.sha256)
}

# Verify all backups
function VERIFY_ALL() {
  log_header "ПРОВЕРКА ВСЕХ БЭКАПОВ"
  for dir in "${BACKUP_ROOT}"/*; do
    [[ -d "$dir" && -f "$dir/checksums.sha256" ]] || continue
    log_info "Проверка: ${dir}"
    (cd "$dir" && sha256sum -c checksums.sha256)
  done
  log_ok "Проверка завершена."
}

# Info about backups
function INFO_BACKUPS() {

    log_header "ИНФОРМАЦИЯ О БЭКАПАХ"

    if [[ ! -d "${BACKUP_ROOT}" ]]; then
        log_error "Каталог ${BACKUP_ROOT} не найден."
        return 1
    fi

    local dirs=()
    mapfile -t dirs < <(ls -1dt "${BACKUP_ROOT}"/*/ 2>/dev/null)

    if (( ${#dirs[@]} == 0 )); then
        log_error "Бэкапы не найдены."
        return 1
    fi

    echo ""
    echo "Имеющиеся каталоги:"

    for d in "${dirs[@]}"; do
        printf "  %s\n" "$(basename "$d")"
    done

    echo ""
    read -r -p "Введите имя каталога (пример: 10-11-2025_04-42): " DATE_IN

    BACKUP_DIR="${BACKUP_ROOT}/${DATE_IN}"

    if [[ ! -d "${BACKUP_DIR}" ]]; then
        log_error "Каталог '${DATE_IN}' не найден."
        return 1
    fi

    echo -e "\n\033[1;36mВыбран каталог: ${DATE_IN}\033[0m\n"

    log_info "Содержимое каталога:"
    printf "%-30s %-10s %-20s\n" "ФАЙЛ" "РАЗМЕР" "ИЗМЕНЁН"
    echo "---------------------------------------------------------------"

    find "${BACKUP_DIR}" -maxdepth 1 -type f -printf "%f\t%10s\t%TY-%Tm-%Td %TH:%TM\n" \
        | awk -F'\t' '{ printf "%-30s %-10.1f MB %-20s\n", $1, $2/1024/1024, $3 }'

    echo "---------------------------------------------------------------"

    local total
    total=$(du -ch "${BACKUP_DIR}"/*tar.* 2>/dev/null | grep total || true)
    log_info "Общий размер архивов: ${total}"

    if [[ -f "${BACKUP_DIR}/checksums.sha256" ]]; then
        log_info "Проверка контрольных сумм:"
        (cd "${BACKUP_DIR}" && sha256sum -c checksums.sha256)
    fi
}

###############################
### === RECOVERY / MAKEPART === ###
###############################

function RECOVERY_TAR() {
    local TARGET_DEV="$1"

    # 1) базовые проверки аргумента
    if [[ -z "$TARGET_DEV" ]]; then
        log_error "Не указан диск для восстановления! Использование: -r /dev/sdX"
        exit 1
    fi

    if [[ ! -b "$TARGET_DEV" ]]; then
        log_error "$TARGET_DEV не является блочным устройством!"
        exit 1
    fi

    # 2) запрещаем опасные операции: восстановление на системный диск в живой системе
    if [[ "$TARGET_DEV" == "$SYS_DEV" && "${LIVE_MODE:-false}" == "true" ]]; then
        log_error "Попытка восстановления на системный диск в живой системе!"
        log_info "Запусти скрипт из rescue или Live-среды."
        exit 1
    fi

    log_info "Восстановление будет выполняться на ${TARGET_DEV}"
    log_separator

    # 3) проверяем окружение бэкапа (BACKUP_ROOT, сам бэкап и т.п.)
    CHECK_BACKUP_ENV

    # 4) подтверждение от пользователя
    log_header "ВОССТАНОВЛЕНИЕ СИСТЕМЫ НА ${TARGET_DEV}"
    read -r -p "Введите 'YES' для продолжения (всё на ${TARGET_DEV} будет перезаписано): " CONFIRM
    [[ "${CONFIRM}" != "YES" ]] && { log_info "Отменено пользователем."; return 1; }

    # 5) определяем номера разделов по ожидаемой схеме и suffix для nvme/mmcblk
    local PART_SUFFIX="" t_efi t_boot t_root t_var t_home t_swap
    [[ "${TARGET_DEV}" =~ (nvme|mmcblk) ]] && PART_SUFFIX="p"

    # Здесь мы предполагаем ту же разметку, что создаёт MAKE_PART:
    # 1 EFI, 2 Boot, 3 RootFS, 4 Var, 5 Swap, 6 HomeDirs
    t_efi="${TARGET_DEV}${PART_SUFFIX}1"
    t_boot="${TARGET_DEV}${PART_SUFFIX}2"
    t_root="${TARGET_DEV}${PART_SUFFIX}3"
    t_var="${TARGET_DEV}${PART_SUFFIX}4"
    t_swap="${TARGET_DEV}${PART_SUFFIX}5"
    t_home="${TARGET_DEV}${PART_SUFFIX}6"

    # 6) подготовка точки монтирования (используем SYS_DIR как временную точку, но НЕ меняем SYS_DEV)
    # создаём временное дерево и монтируем разделы целевого диска
    mkdir -p "${SYS_DIR}"
    # Помним: если ранее LIVE_MODE был true — мы всё равно работаем в оффлайн-режиме для целевого диска
    LIVE_MODE=false

    # Монтируем корень целевого диска
    if [[ -b "${t_root}" ]]; then
        mount ${TYPEFS} "${t_root}" "${SYS_DIR}" || { log_error "Не удалось примонтировать ${t_root} → ${SYS_DIR}"; return 1; }
        log_ok "Успешно смонтирован ${t_root} → ${SYS_DIR}"
    else
        log_error "Ожидаемая RootFS-партция ${t_root} не найдена!"
        return 1
    fi

    # создаём и монтируем вспомогательные точки (инициализация)
    mkdir -p "${SYS_DIR}/boot" "${SYS_DIR}/boot/efi" "${SYS_DIR}/home" "${SYS_DIR}/var"

    if [[ -b "${t_boot}" ]]; then
        mount "${t_boot}" "${SYS_DIR}/boot" 2>/dev/null && log_ok "Монтирован ${t_boot} → ${SYS_DIR}/boot" || log_warn "Не удалось смонтировать ${t_boot} → ${SYS_DIR}/boot"
    else
        log_warn "Boot-партция ${t_boot} не найдена — /boot будет восстановлен в корневой FS."
    fi

    if [[ -b "${t_efi}" ]]; then
        mount "${t_efi}" "${SYS_DIR}/boot/efi" 2>/dev/null && log_ok "Монтирован ${t_efi} → ${SYS_DIR}/boot/efi" || log_warn "Не удалось смонтировать ${t_efi} → ${SYS_DIR}/boot/efi"
    else
        log_warn "EFI-партция ${t_efi} не найдена — пропускаем установку EFI-раздела."
    fi

    if [[ -b "${t_home}" ]]; then
        mount "${t_home}" "${SYS_DIR}/home" 2>/dev/null && log_ok "Монтирован ${t_home} → ${SYS_DIR}/home" || log_warn "Не удалось смонтировать ${t_home} → ${SYS_DIR}/home"
    else
        log_warn "Home-партция ${t_home} не найдена — каталоги Homes будут находиться в корневой FS."
    fi

    if [[ -b "${t_var}" ]]; then
        mount "${t_var}" "${SYS_DIR}/var" 2>/dev/null && log_ok "Монтирован ${t_var} → ${SYS_DIR}/var" || log_warn "Не удалось смонтировать ${t_var} → ${SYS_DIR}/var"
    else
        log_warn "Var-партция ${t_var} не найдена — /var будет восстановлен в корневой FS."
    fi

    # 7) устанавливаем PARTS и порядок для последующего восстановления
    SET_PARTS

    # 8) перебираем архивы в BACKUP_PATH и распаковываем в целевые точки (т.е. внутрь SYS_DIR)
    for arc in "${BACKUP_PATH}"/*tar.*; do
        [[ ! -f "$arc" ]] && continue
        name=$(basename "$arc")
        base="${name%%.tar*}"

        # map base -> destination inside mounted SYS_DIR
        case "$base" in
            efi) dest="${SYS_DIR}/boot/efi" ;;
            boot) dest="${SYS_DIR}/boot" ;;
            rootfs) dest="${SYS_DIR}" ;;
            home) dest="${SYS_DIR}/home" ;;
            var) dest="${SYS_DIR}/var" ;;
            *) dest="${SYS_DIR}" ;;
        esac

        mkdir -p "${dest}"
        log_info "Восстановление: ${arc} → ${dest}"

        case "$arc" in
            *.tar.zst|*.zst)
                zstd -d -c "$arc" | tar "${TAR_EXTRACT_OPTS[@]}" xpf - -C "${dest}" ;;
            *.tar.xz|*.xz)
                xz -d -c "$arc" | tar "${TAR_EXTRACT_OPTS[@]}" xpf - -C "${dest}" ;;
            *.tar)
                tar "${TAR_EXTRACT_OPTS[@]}" xpf "$arc" -C "${dest}" ;;
            *)
                log_warn "Неизвестный формат: $arc — пропускаем." ;;
        esac && log_ok "Успешно восстановлен ${base}"
    done

    # 9) синхронизируем и размонтируем целевой временный root
    sync || true

    log_info "Размонтирование целевого дерева ${SYS_DIR}..."
    # размонтируем в обратном порядке (try best-effort)
    umount "${SYS_DIR}/boot/efi" 2>/dev/null || true
    umount "${SYS_DIR}/boot" 2>/dev/null || true
    umount "${SYS_DIR}/home" 2>/dev/null || true
    umount "${SYS_DIR}/var" 2>/dev/null || true
    umount "${SYS_DIR}" 2>/dev/null || true

    # 10) Установка загрузчика в целевую систему
    log_header "УСТАНОВКА ЗАГРУЗЧИКА НА ${TARGET_DEV}"
    # смонтируем снова для chroot-установки (если нужно)
    mkdir -p "${SYS_DIR}"
    mount "${t_root}" "${SYS_DIR}" 2>/dev/null || { log_error "Не удалось монтировать ${t_root} для установки загрузчика"; return 1; }
    mkdir -p "${SYS_DIR}/boot" "${SYS_DIR}/boot/efi"
    [[ -b "${t_boot}" ]] && mount "${t_boot}" "${SYS_DIR}/boot" 2>/dev/null || true
    [[ -b "${t_efi}" ]] && mount "${t_efi}" "${SYS_DIR}/boot/efi" 2>/dev/null || true

    if [[ -d "${SYS_DIR}/sys/firmware/efi" || -d "${SYS_DIR}/boot/efi" ]]; then
        log_info "Обнаружен EFI: установка GRUB для UEFI..."
        chroot "${SYS_DIR}" /bin/bash -c "
            grub2-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Fedora &&
            grub2-mkconfig -o /boot/grub2/grub.cfg &&
            efibootmgr -v
        " && log_ok "GRUB для UEFI установлен успешно" || log_error "Ошибка установки загрузчика в режиме EFI."
    else
        log_info "BIOS: установка GRUB в MBR целевого диска ${TARGET_DEV}..."
        chroot "${SYS_DIR}" /bin/bash -c "
            grub2-install ${TARGET_DEV} &&
            grub2-mkconfig -o /boot/grub2/grub.cfg
        " && log_ok "GRUB для BIOS установлен успешно" || log_error "Ошибка установки загрузчика в режиме BIOS."
    fi

    # финальное синхронизирование и размонтирование
    sync || true
    umount "${SYS_DIR}/boot/efi" 2>/dev/null || true
    umount "${SYS_DIR}/boot" 2>/dev/null || true
    umount "${SYS_DIR}/home" 2>/dev/null || true
    umount "${SYS_DIR}/var" 2>/dev/null || true
    umount "${SYS_DIR}" 2>/dev/null || true

    log_ok "Установка загрузчика завершена."
    log_ok "Восстановление на ${TARGET_DEV} завершено."
}


function MAKE_PART() {
    local TARGET_DEV="$1"

    if [[ -z "$TARGET_DEV" ]]; then
        log_error "Не указан диск для разметки!"
        exit 1
    fi

    # Проверяем, что устройство существует
    if [[ ! -b "$TARGET_DEV" ]]; then
        log_error "$TARGET_DEV не является блочным устройством!"
        exit 1
    fi

    # Определяем реальный системный диск (диск, где смонтирован /)
    local REAL_SYSTEM_DEV PARTNAME
    PARTNAME=$(findmnt -n -o SOURCE /)
    REAL_SYSTEM_DEV="/dev/$(lsblk -no PKNAME "$PARTNAME")"

    # Запрещаем разметку системного диска в живой системе
    if mountpoint -q / && [[ "$TARGET_DEV" == "$REAL_SYSTEM_DEV" ]]; then
        log_error "Попытка разметки системного диска ($TARGET_DEV) в живой системе!"
        log_info "Запустите скрипт в режиме rescue/offline."
        exit 1
    fi

    log_info "Разметка и подготовка ${TARGET_DEV}"
    CHECK_BACKUP_ENV

    local DST_DEV="$TARGET_DEV"

    log_header "СОЗДАНИЕ РАЗДЕЛОВ НА ${DST_DEV}"
    log_warn "ВНИМАНИЕ: Все данные на устройстве будут уничтожены!"

    log_info "Текущая структура разделов:"
    lsblk "${DST_DEV}"

    read -r -p "Введите 'YES' для продолжения: " CONFIRM
    [[ "${CONFIRM}" != "YES" ]] && { log_info "Отменено пользователем."; return 1; }

    log_info "Создание GPT разметки на ${DST_DEV}"
    parted -a optimal -s "${DST_DEV}" mklabel gpt

    log_info "Создание разделов..."
    parted -a optimal -s "${DST_DEV}" \
        mkpart primary fat32 1MiB 101MiB \
        set 1 esp on \
        name 1 "EFI" \
        mkpart primary xfs 101MiB 2149MiB \
        name 2 "Boot" \
        mkpart primary xfs 2149MiB 102149MiB \
        name 3 "RootFS" \
        mkpart primary xfs 102149MiB 117349MiB \
        name 4 "Var" \
        mkpart primary linux-swap 117349MiB 132549MiB \
        name 5 "Swap" \
        mkpart primary xfs 132549MiB 100% \
        name 6 "HomeDirs"

    local PART_SUFFIX=""
    [[ "${DST_DEV}" =~ (nvme|mmcblk) ]] && PART_SUFFIX="p"

    log_info "Форматирование разделов..."
    mkfs.fat -F32 -n EFI "${DST_DEV}${PART_SUFFIX}1" && log_ok "EFI раздел отформатирован"
    mkfs.xfs -f -L BOOT  "${DST_DEV}${PART_SUFFIX}2" && log_ok "Boot раздел отформатирован"
    mkfs.xfs -f -L ROOTFS "${DST_DEV}${PART_SUFFIX}3" && log_ok "RootFS раздел отформатирован"
    mkfs.xfs -f -L VAR  "${DST_DEV}${PART_SUFFIX}4" && log_ok "Var раздел отформатирован"
    mkswap -L SWAP "${DST_DEV}${PART_SUFFIX}5" && log_ok "Swap раздел создан"
    mkfs.xfs -f -L HOMES "${DST_DEV}${PART_SUFFIX}6" && log_ok "Home раздел отформатирован"

    log_ok "Разметка и форматирование диска ${DST_DEV} завершены."

    log_info "Итоговая структура разделов:"
    lsblk -f "${DST_DEV}"
}

###############################
### === HELP / MAIN === ###
###############################
function HELP() {
  cat <<EOF
${ME} — резервное копирование системы TAR-архивами

Использование:
  -b    создать резервные архивы (backup)
  -r [УСТРОЙСТВО]  восстановить систему (recovery)
  -c [УСТРОЙСТВО]  создать структуру разделов на диске
  -i    показать информацию о бэкапах и предложить проверку
  -v    проверить контрольные суммы для указанной даты (ручная проверка)
  -V    если вместе с -b -> проверить только что созданный бэкап;
        если один -> проверить ВСЕ бэкапы (sha256sum -c)
  -h    справка

Логирование:
  Уровень детализации: ERROR(1) < WARN(2) < INFO(3) < DEBUG(4)
  По умолчанию: INFO(3). Для отладки: LOG_LEVEL=4 $ME -b

Примеры:
  $ME -b                          # Создать бэкап
  $ME -r /dev/sdb                 # Восстановить систему на /dev/sdb
  $ME -c /dev/sdb                 # Создать разделы на /dev/sdb
  $ME -i                          # Показать информацию о бэкапах
  LOG_LEVEL=2 $ME -b              # Только предупреждения и ошибки

Версия: 3.5-stable (с профессиональным логированием)
EOF
}

# parse options — allow combined flags; track actions
DO_BACKUP=false
DO_RECOVER=false
DO_MAKEPART=false
DO_INFO=false
DO_VERIFY_DATE=false
DO_VERIFY_ALL=false
VERIFY_AFTER=false
MAKEPART_ARG=""
RECOVER_ARG=""

while getopts "br:c:ivVh" opt; do
  case $opt in
    b) DO_BACKUP=true ;;
    r) DO_RECOVER=true
       RECOVER_ARG="$OPTARG" ;;
    c) DO_MAKEPART=true
       MAKEPART_ARG="$OPTARG";;
    i) DO_INFO=true ;;
    v) DO_VERIFY_DATE=true ;;
    V) DO_VERIFY_ALL=true ;;
    h) HELP; exit 0 ;;
    *) HELP; exit 1 ;;
  esac
done

# If both -b and -V passed, set VERIFY_AFTER to check created backup
if [[ "$DO_BACKUP" == true && "$DO_VERIFY_ALL" == true ]]; then
  VERIFY_AFTER=true
fi

# Run actions in sensible order
if [[ "$DO_BACKUP" == true ]]; then
  CREATE_TAR
  if [[ "$VERIFY_AFTER" == true ]]; then
    log_header "АВТОМАТИЧЕСКАЯ ПРОВЕРКА СОЗДАННОГО БЭКАПА"
    VERIFY_BACKUP_DATE "${BACKUP_PATH}" || log_warn "Проверка обнаружила ошибки."
  fi
fi

if [[ "$DO_VERIFY_ALL" == true && "$DO_BACKUP" != true ]]; then
  VERIFY_ALL
fi

if [[ "$DO_VERIFY_DATE" == true ]]; then
  PROMPT_VERIFY_BACKUP_DIR
fi

if [[ "$DO_INFO" == true ]]; then
  INFO_BACKUPS
fi

if [[ "$DO_MAKEPART" == true ]]; then
  MAKE_PART "${MAKEPART_ARG:-}"
fi

if [[ "$DO_RECOVER" == true ]]; then
    PROMPT_VERIFY_BACKUP_DIR
    BACKUP_PATH="$SELECTED_DIR"
    RECOVERY_TAR "$RECOVER_ARG"
  RECOVERY_TAR "${RECOVER_ARG:-}"
fi

log_ok "Скрипт завершил работу"
exit 0