Архивирование системы нового поколения
Архитектура скрипта (конспект, чтобы быстро сориентироваться)
Высокий приоритет (улучшения):
Средний/низкий:
- Цель
- Унифицированный инструмент резервного копирования и восстановления Fedora-подобных систем (tar-архивы: efi, boot, rootfs, home, var).
- Поддержка живой (live) и оффлайн (rescue) работы.
- Простая разметка диска (MAKE_PART), проверка и автоматическое приведение /etc/fstab при восстановлении.
- Ключевые переменные
- SYS_DEV — устройство «системного» диска (по умолчанию /dev/nvme0n1 в текущей конфигурации).
- SYS_DIR — точка монтирования для оффлайн-восстановления (по умолчанию /mnt/fedora).
- BACKUP_ROOT / BACKUP_PATH — корень и каталог конкретного бэкапа.
- TYPEFS — опция типа при монтировании (например -t xfs).
- COMPRESSOR — выбранный компрессор (zstd/xz) исходя из типа носителя.
- Основные функции
- 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.
- Ключевая логика действий
- При 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 и корректное размонтирование.
- Особенности/решения, принятые в процессе
- Грубая замена 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 не ругался.
- TODO-list (кратко, приоритеты)
- Критично (сделать/проверить перед переносом):
- Верифицировать финальный CREATE_TAR (версия по типу FS или единая) — ты выбрал вариант по фактическому типу ФС, убедиться, что TAR_CREATE_OPTS корректно применяются для каждого типа.
- Добавить защиту на случай неполного BACKUP_PATH (если один архив отсутствует — продолжать и явно пометить).
- Дополнительно логировать время работы по каждому архиву (начало/конец) — это поможет в анализе долгих rootfs запусков.
- Проверить восстановление /etc/fstab на edge-cases (пустые UUID, отсутствующие разделы) — не перезаписывать «пустыми» значениями.
- Обработать поведение efibootmgr: дубли-entries (возможность пометить и удалять старые записи с таким же Label при желании).
Высокий приоритет (улучшения):
- Реализовать режим «быстрой» архивации (исключить var/home при необходимости).
- Поддержка мультисжатия: возможность задавать компрессор per-part (например: efi — xz маленький, rootfs — zstd -9).
- Добавить опцию —dry-run для CREATE_TAR/RECOVERY_TAR (что будет сделано, без выполнения).
- Повысить устойчивость к SIGINT: завершение операции должно аккуратно убирать tmp-файлы ${ARC}.tmp.
Средний/низкий:
- Интеграция с systemd timer / unit для автоматических бэкапов.
- Поддержка LUKS — detect + временное открытие в recovery.
- Поддержка btrfs snapshots (альтернативная ветка).
- Код: выделить все
#!/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