#!/bin/sh -efu
### This file is covered by the GNU General Public License
### version 3 or later.
###
### Copyright (C) 2019, ALT Linux Team
### Author: Leonid Krivoshein <klark@altlinux.org>

### Пример "универсального" скрипта для создания резервной копии
### установленной операционной системы ALT Linux, версия 0.1.0


# Под каким именем будет сохранён очередной бэкап (каталог).
# По умолчанию используется текущая дата в формате YYYY-MM-DD.
#
backup_name="$(LC_TIME=C date "+%F")"

# Хранилище бэкапов: точка монтирования либо каталог
backup_base="/mnt/autorun"

# ID записи UEFI-загрузки, используемый по умолчанию
default_loader_id="altlinux"

# Алгоритмы вычисляемыех контрольных сумм (md5, sha1, sha256, ...)
digests="sha256"

# Максимальная длина имени архива
archive_name_limit=16

# Порядковый номер найденного /etc/fstab для
# авто-определения нужного корневого раздела
#
rootfs_number=

# 1, если нужно просканировать все устройства, вывести список
# корневых разделов с названиями/версиями ОС и завершить работу
#
scan_only=0

# 1, чтобы показывать выполняемые действия, но ничего не делать
dryrun=0

# 1, если целевой компьютер может умереть при записи данных в NVRAM
no_nvram=0

# 1, если исходные файловые системы необходимо почистить до сохранения
cleanup=0

# 1, если в исходной системе нужно переименовать единственный сетевой интерфейс
rename_iface=0

# 1, если требуется более подробный вывод сообщений (режим отладки)
verbose=0

# 1, если необходимо подавить вывод сообщений в стандартный поток вывода
quiet=0

# 1, если необходимо отмонтировать $backup_base по окончанию работы
need_unmount=0

# 1, если съёмные носители должны считаться пригодными целевыми дисками
removable=0

# Список точек монтирования, по которым определяются защищаемые диски
protected_mpoints="/ /image /mnt/backup $backup_base"

# Игнорируемые при поиске целых дисков начальные имена устройств
skip_disk_names="ram fd loop sr"

# Опции tar для создания бэкапов
tar_opts="--numeric-owner --one-file-system"
tar_opts="$tar_opts --warning=no-file-ignored"
tar_opts="$tar_opts --transform='s,^\\.\\/,,'"

# Опции монтирования разделов FAT и ESP (EFI System Partition)
vfatopts="noatime,umask=0,quiet,showexec,iocharset=utf8,codepage=866"

# Опции монтирования раздела /boot
bootopts="noatime,nodiratime,nodev,noexec"

# Опции монтирования корневого раздела
rootopts="noatime,nodiratime"

# Опции монтирования разделов данных
dataopts="noatime,nodiratime,nosuid"

# Определяемый дополнительными опциями командной строки список
# устройств или точек монтирования в целевой системе, которые
# бэкапить не следует
#
skip_volumes=

# Определяемый дополнительными опциями командной строки список
# исключений (шаблонов типа "mnt/*"), которые бэкапить не следует
#
excludes='lost+found mnt/target'

# Автоматически определяемые в процессе работы скрипта
# имена устройств разделов, которые будут сохранены
# (обязательным является только "корневой" раздел):
#
# $esp_part соответствует /boot/efi
# $bootpart соответствует /boot
# $swappart соответствует SWAP
# $rootpart соответствует /
# $var_part соответствует /var
# $homepart соответствует /home
#
esp_part=
bootpart=
swappart=
rootpart=
var_part=
homepart=

# Все остальные разделы сохраняются в виде списка элементов,
# отделяемых одним пробелом в формате <device>:<fstype>:<mpoint>
#
other_parts=

# Автоматически определяемый список защищаемых от бэкапа устройств
protected_devices=

# Автоматически определяемый идентификатор записи UEFI-загрузки
boot_loader_id=

# Изначально: определяемый в командной строке целевой диск или раздел.
# В процессе работы скрипта: эта переменная получит список всех дисков
# (родительских устройств), на которых расположены сохраняемые разделы.
#
targets=

# Способ определения целевых устройств
tgtway="auto"

# 1 Мегабайт
MiB=$((1024 * 1024))

# Название данного скрипта
progname="${0##*/}"


# Выводит подсказку и завершает работу
#
show_help() {
	echo "Использование: $progname [<опции>...] [--] [<цель>]"
	echo
	echo "Цель - это блочное устройство (диск или раздел), которое будет"
	echo "просканировано, чтобы найти хотя бы один корневой раздел Linux."
	echo "По нему определяются другие разделы, подлежащие сохранению. По"
	echo "умолчанию сохраняются все найденные таким образом разделы. Если"
	echo "на диске имеется более одной ОС Linux, обязательно указывать"
	echo "порядковый номер нужной ОС, иначе ничего не будет сохранено."
	echo "Цель указывать необязательно - в этом случае сканируются все"
	echo "доступные блочные устройства, за исключением защищаемых."
	echo
	echo "Опции:"
	echo
	echo "-b, --base <DIR>    Базовый каталог для сохранения всех бэкапов."
	echo "-n, --name <NAME>   Сохранить создаваемый бэкап под этим именем."
	echo "-s, --skip <LIST>   Не сохранять перечисленные через запятую"
	echo "                    разделы, диски и точки монтирования. UUID=id"
	echo "                    и LABEL=метка допустимы, данную опцию можно"
	echo "                    указывать более одного раза."
	echo "-S, --scan-only     Только вывести список операционных систем."
	echo "-X, --exclude <PAT> Добавляет указанный шаблон к исключениям."
	echo "                    Данную опцию можно указать несколько раз."
	echo "-f, --fstab <NO>    Сохранить указанную по счёту Linux-систему."
	echo "-r, --removable     Разрешить использование съёмных носителей."
	echo "-R, --rename-iface  Переименовать единственный сетевой интерфейс."
	echo "-c, --cleanup       Очищать файловые системы перед сохранением."
	echo "-p, --no-nvram      Запретить запись каких-либо данных в NVRAM."
	echo "-t, --tar <LOPTS>   Добавить перечисленные через запятую длинные"
	echo "                    опции при запуске программы tar, данную опцию"
	echo "                    можно указывать более одного раза. Например,"
	echo "                    -t xattrs,acls добавит опции --xattrs --acls."
	echo "-U, --uuid <UUID>   Взять раздел с этим UUID в качестве целевого."
	echo "-L, --label <LABEL> Взять раздел с этой меткой в качестве целевого."
	echo "-C, --current,self  Сохранить текущую систему, с которой грузились."
	echo "-l, --loader <ID>   Использовать этот идентификатор UEFI-загрузки."
	echo "-A, --algo <LIST>   Список алгоритмов контрольных сумм через запятую."
	echo "-N, --no-act,dryrun Только показать намерения, но ничего не делать."
	echo "-v, --verbose       Выводить больше деталей в сообщениях (отладка)."
	echo "-q, --quiet         Ничего не выводить в стандартный поток вывода."
	echo "-h, --help          Показать данную подсказку и завершить работу."
	echo
	echo "Пожалуйста, сообщайте об ошибках на https://bugzilla.altlinux.org/"
	exit ${1-0}
}

# Обеспечивает фатальный выход с выводом сообщения об ошибке
#
fatal() {
	echo "$progname СБОЙ: $*" >&2
	exit 1
}

# Выводит отладочное сообщение
#
debug() {
	[ $verbose -eq 0 ] || echo "$*" >&2
}

# Выводит сообщение в стандартный поток вывода
#
msg() {
	[ $quiet -ne 0 ] || echo "$*"
}

# Выполняет или не выполняет указанные действия, в зависимости
# от режима выполнения, с выводом сообщения в отладочный журнал
#
run() {
	debug "Executing: $*"
	[ $dryrun -ne 0 ] || "$@"
}

# Конвертирует байты в мегабайты с округлением до ближайшего целого
#
size2mib() {
	local m=$(($1 / $MiB))

	[ $(($1 % $MiB)) -eq 0 ] || m=$(($m + 1))
	echo -n "$m"
}

# Выводит сортированный список, соответствующий указанному шаблону
#
glob() {
	(set +f; eval ls -X1 -- "$@" ||:) 2>/dev/null
}

# В реальном режиме выполнения рекурсивно сохраняет все файлы
# текущего каталога в указанный архив $1 при помощи tar и pigz,
# в режиме симуляции только показывает запускаемые команды
#
backup_linux() {
	local archive="$1"; shift
	local mpoint="$1"; shift
	local i pv options="$tar_opts"

	msg "Сохраняется $mpoint..."

	for i in $excludes; do
		options="$options --exclude='$i'"
	done

	debug "Executing: cd '$(pwd)' &&"
	debug "Executing: tar -cpSf - $options $* |"
	debug "Executing: pigz -9qnc > '$backupdir/$archive.tgz'"

	[ $dryrun -eq 0 ] || return 0

	pv="$(command -v pv 2>/dev/null ||:)"
	if [ $verbose -ne 0 -o $quiet -ne 0 -o ! -x "$pv" ]
	then
		eval tar -cpSf - $options "$@" |
			pigz -9qnc > "$backupdir/$archive.tgz"
	else
		eval tar -cpSf - $options "$@" |"$pv" -rbt |
			pigz -9qnc > "$backupdir/$archive.tgz"
	fi
}

# Возвращает 0, если указанный каталог $1 уже смонтирован
#
is_dir_mounted() {
	awk '{print $2;}' /proc/mounts |grep -qw -- "$1" || return 1
}

# Выводит каталог с точкой монтирования, если указанное
# устройство $1 уже смонтировано с файловой системой $2
#
device_mpoint() {
	local devname="${1##/dev/}" fstype="$2"
	local mp name sysfs blkdev=

	mp="$(awk '{print $1 " " $2 " " $3;}' /proc/mounts |
		grep "/dev/$devname " |grep -- " $fstype" |
		awk '{print $2;}' |tail -n1)"
	if [ "${mp:0:1}" = "/" -a -d "$mp" ]; then
		echo -n "$mp"
		return 0
	fi

	case "$devname" in
	dm-[0-9]*)
		sysfs="$(mountpoint -x "/dev/$devname" 2>/dev/null ||:)"
		[ -n "$sysfs" ] || return 0
		sysfs="$(readlink -fv "/sys/dev/block/$sysfs" 2>/dev/null ||:)"
		[ -n "$sysfs" -a -f "$sysfs/dm/name" ] || return 0
		read name < "$sysfs/dm/name"
		[ -n "$name" ] || return 0
		blkdev="$(readlink -fv "/dev/mapper/$name" 2>/dev/null ||:)"
		[ "$blkdev" = "/dev/$devname" ] || return 0
		mp="$(awk '{print $1 " " $2 " " $3;}' /proc/mounts |
			grep "/dev/mapper/$name " |grep -- " $fstype" |
			awk '{print $2;}' |tail -n1)"
		[ "${mp:0:1}" = "/" -a -d "$mp" ] && echo -n "$mp" ||:
		;;
	esac

	return 0
}

# Размонтирует указанный каталог $1, если он смонтирован
#
cond_unmount() {
	is_dir_mounted "$1" && umount -fl -- "$1" >/dev/null 2>&1 ||:
}

# Рекурсивно размонтирует в нужном порядке всё, что смонтировано в /mnt/target
#
unmount_all_targets() {
	local mp list="$(grep -sE " \/mnt\/target[ \/]" /proc/mounts |
				sort -k2,2 -rs |awk '{print $2;}')"

	if [ -n "$list" ]; then
		debug "Unmounting target filesystems..."
		for mp in $list; do
			debug "  - unmounting $mp..."
			cond_unmount "$mp"
		done
		debug
	fi
}

# Очищающий следы работы скрипта обработчик завершения работы
#
exit_handler() {
	local rc=$?

	trap - EXIT; cd /
	if [ "$backup_base" = "/mnt/autorun" ] &&
		grep -qs ' /mnt/autorun ext2 r' /proc/mounts
	then
		if [ $need_unmount -ne 0 ]; then
			umount /mnt/autorun >/dev/null 2>&1 &&
				debug "Unmounting /mnt/autorun (alt-autorun) media..." ||
				need_unmount=0
		fi
		if [ $need_unmount -eq 0 ]; then
			debug "Remounting /mnt/autorun for read-only access..."
			mount -o remount,ro /mnt/autorun ||:
		fi
	fi
	[ ! -d /mnt/target ] || unmount_all_targets
	[ -z "${probe_dir-}" ] || cond_unmount "${probe_dir-}"
	[ ! -d "$workdir" ] || rm -rf --one-file-system -- "$workdir"
	rmdir /mnt/target 2>/dev/null ||:
	exit $rc
}

# Выводит тип файловой системы для указанного устройства $1
#
getfstype() {
	blkid -o value -s TYPE "$1" 2>/dev/null ||:
}

# Выводит размер устройства в мегабайтах
#
get_devsize() {
	local bytes=$(blockdev --getsize64 "$1" 2>/dev/null ||:)
	[ -z "$bytes" -o "$bytes" = "0" ] && echo -n "0" || size2mib $bytes
}

# Сохраняет мета-данные устройства $1 в файлы с именем $2.*
#
save_fsmeta() {
	local regex tag="$(getfstype "$1")"

	case "$tag" in
	ext2|ext3|ext4|ext4dev)
		regex='/^Filesystem features:[[:space:]]*/!d'
		regex="$regex;s/^[^:]*:[[:space:]]*//"
		dumpe2fs "$1" 2>/dev/null |sed -e "$regex" |
				tr ' ' '\n' > "$metadir/$2.e2fs"
		;;
	esac
	tag=$(get_devsize "$1")
	echo -n "$tag" > "$metadir/$2.size"
	tag="$(blkid -o value -s LABEL "$1" 2>/dev/null ||:)"
	[ -z "$tag" ] || echo -n "$tag" > "$metadir/$2.label"
	tag="$(blkid -o value -s UUID "$1" 2>/dev/null ||:)"
	[ -z "$tag" ] || echo -n "$tag" > "$metadir/$2.uuid"
}

# Возвращает 0, если блочное устройство $1 имеет файловую систему $2
#
expect_fs() {
	[ -b "$1" -a "$(getfstype "$1")" = "$2" ] || return 1
}

# Возвращает 0 в случае Linux-совместимой файловой системы $1
#
is_linux_fs() {
	case "$1" in
	ext2|ext3|ext4|ext4dev|jfs|xfs|reiserfs|btrfs|zfs)
		return 0;;
	esac
	return 1
}

# Убеждается, что блочное устройство $1 для точки монтирования
# $2 имеет Linux-совместимую файловую систему, и в случае успеха
# возвращает 0, иначе генерирует соответствующее фатальное
# сообщение и завершает работу скрипта
#
must_be_linux() {
	[ -b "$1" ] || fatal "Ошибочный раздел: $1 ($2)"
	local fs="$(getfstype "$1")"; is_linux_fs "$fs" ||
		fatal "Неподдерживаемая файловая система '$fs' на разделе $1 ($2)"
	return 0
}

# Ищет элемент $1 в массиве $* и возвращет 0 в случае успеха
#
in_array() {
	local needle="$1" item=""; shift

	for item in $*; do
		[ "$item" != "$needle" ] || return 0
	done

	return 1
}

# Выводит карту подключенных носителей в формате "major:minor <TAB> devname",
# исключая те, чьи имена начинаются с перечисленных в списке $skip_disk_names
#
show_devmap() {
	local f major minor dev b=0

	[ -f /proc/partitions ] ||
		fatal "Вероятно, файловая система /proc не смонтирована!"
	sed -e '3,$!d' /proc/partitions |
	while read major minor b dev
	do
		b=0
		if [ -n "$skip_disk_names" ]; then
			for f in $skip_disk_names; do
				case "$dev" in
				${f}[0-9]*)
					b=1
					break
					;;
				esac
			done
			[ $b -eq 0 ] || continue
		fi
		echo -e "$major:$minor\t$dev"
	done
}

# Определяет по ранее созданной карте носителей
# и имени узла /dev/$1 его major:minor номер
#
get_devno() {
	local needle="$1" devno devname

	while read devno devname; do
		if [ "$needle" = "$devname" ]; then
			echo -n "$devno"
			break
		fi
	done < "$workdir"/devices
}

# Рекурсивно определяет и выводит список всех
# родительских устройств для указанного /dev/$1
#
get_all_parents() {
	local devname="$1" devno sysfs number

	[ -b "/dev/$devname" ] || return 0

	# Если $devname является разделом диска
	if [ ! -f "/sys/block/$devname/dev" ]; then
		if [ -f "/sys/class/block/$devname/partition" ]; then
			read number < "/sys/class/block/$devname/partition"
			devname="${devname%$number}"
			[ -b "/dev/$devname" ] ||
				devname="${devname%p}"
			if [ -b "/dev/$devname" ]; then
				devname="$(readlink -fv "/dev/$devname")"
				case "$devname" in
				/dev/*)	devname="${devname##/dev/}";;
				*)	devname="";;
				esac
				if [ -b "/dev/$devname" ]; then
					get_all_parents "$devname"
					echo "$devname"
				fi
			fi
		fi
	fi

	# Если устройство находится на других устройствах
	devno="$(get_devno "$devname")"
	[ -n "$devno" ] || return 0
	sysfs="$(readlink -fv "/sys/dev/block/$devno")"
	[ -d "$sysfs" ] || return 0
	if [ -f "$sysfs/dev" -a -d "$sysfs/slaves" ]; then
		read number < "$sysfs/dev"
		if [ "$number" = "$devno" ]; then
			sysfs="$(glob "$sysfs/slaves/*")"
			if [ -n "$sysfs" ]; then
				for devname in $sysfs; do
					devname="${devname##*/}"
					if [ -b "/dev/$devname" ]; then
						get_all_parents "$devname"
						echo "$devname"
					fi
				done
			fi
		fi
	fi
}

# Возвращает 0, если указанное устройство /dev/$1 защищено от бэкапа
#
is_protected() {
	[ -n "$protected_devices" ] && in_array "$1" $protected_devices || return 1
}

# Проверяет целевой носитель на предмет допустимости использования
# в качестве исходного диска или раздела, с которого можно снимать
# резервную копию
#
check_target_device() {
	local dummy sysfs devno devname

	devname="$(readlink -fv "$targets")"
	case "$devname" in
	/dev/*)	devname="${devname##/dev/}";;
	*)	devname="";;
	esac
	[ -n "$devname" -a -b "/dev/$devname" ] ||
		fatal "Целевой диск ($targets) не является блочным устройством!"
	devno="$(get_devno "$devname")"
	[ -n "$devno" ] || fatal "Ошибка определения номера целевого диска!"
	sysfs="$(readlink -fv "/sys/dev/block/$devno")"
	[ -f "$sysfs/dev" ] || fatal "Целевой диск не найден в /sys/dev/block!"
	read dummy < "$sysfs/dev"
	[ "$dummy" = "$devno" ] || fatal "Обнаружено внутреннее несоответствие!"

	if [ $cleanup -ne 0 ]; then
		[ -r "$sysfs/ro" ] && read dummy < "$sysfs/ro" ||
			dummy="$(blockdev --getro "/dev/$devname")"
		[ "$dummy" = "0" ] ||
			fatal "Целевой диск только для чтения, его нельзя очистить!"
	fi
	if [ $removable -eq 0 -a -r "$sysfs/removable" ]; then
		read dummy < "$sysfs/removable"
		[ "$dummy" = "0" ] ||
			fatal "Съёмные диски следует бэкапить с флагом --removable!"
	fi

	tgtway="part"
	targets="$devname"
	dummy="$(getfstype "/dev/$devname")"
	if is_linux_fs "$dummy"; then
		tgtway="disk"
	elif [ -f "/sys/block/$devname/dev" ]; then
		read dummy < "/sys/block/$devname/dev"
		if [ "$dummy" = "$devno" ]; then
			dummy="$(blkid -o value -s PTTYPE "/dev/$devname" 2>/dev/null ||:)"
			if [ "$dummy" != "dos" -a "$dummy" != "gpt" ]; then
				debug "WARNING: Device /dev/$devname is not MBR/GPT-labeled!"
			else
				dummy="$(glob "/dev/${devname}?*")"
				[ -n "$dummy" ] || fatal "Целевые разделы не найдены!"
				tgtway="disk"; targets=
				for dummy in $(glob "/dev/${devname}?*"); do
					targets="$targets ${dummy##/dev/}"
				done
				targets="${targets## }"
			fi
		fi
	fi
}

# Пересоздаёт списки пропускаемых томов и защищаемых разделов
#
rebuild_skipped_volumes() {
	local dev mp ptl

	if [ -n "$skip_volumes" ]; then
		ptl="$skip_volumes"
		skip_volumes=
		for mp in $ptl; do
			case "$mp" in
			/dev/?*)   dev="$(readlink -fv "$mp")";;
			LABEL=?*)  dev="$(blkid -L "${mp##LABEL=}" 2>/dev/null ||:)";;
			UUID=?*)   dev="$(blkid -U "${mp##UUID=}" 2>/dev/null ||:)";;
			/?*)	   dev="";;
			*)	   debug "Invalid volume '$mp', will be ignored!"
				   continue;;
			esac
			case "$dev" in
			/dev/?*)   dev="${dev##/dev/}";;
			esac
			if [ -z "$dev" -o ! -b "/dev/$dev" ]; then
				skip_volumes="$skip_volumes $mp"
			elif ! is_protected "$dev"; then
				protected_devices="$protected_devices $dev"
			fi
		done
		skip_volumes="${skip_volumes## }"
	fi

	debug "Skip volumes:    ${skip_volumes:-<EMPTY LIST>}"; debug
}

# Определяет список устройств, с которых могла быть
# загружена ОС или на которые может сохраняться бэкап
#
autodetect_protected() {
	local dev mp plist
	local ptl="$protected_mpoints"

	in_array "$backup_base" $ptl || ptl="$ptl $backup_base"

	for mp in $ptl; do
		dev="$(grep -s " $mp " /proc/mounts |tail -n1 |cut -f1 -d ' ')"
		[ -n "$dev" ] && is_dir_mounted "$mp" || continue
		dev="$(readlink -fv "$dev")"
		case "$dev" in
		/dev/*)	dev="${dev##/dev/}";;
		*)	continue;;
		esac
		[ -b "/dev/$dev" ] ||
			continue
		is_protected "$dev" ||
			protected_devices="$protected_devices $dev"
		plist="$(get_all_parents "$dev")"
		[ -n "$plist" ] || continue
		for dev in $plist; do
			is_protected "$dev" ||
				protected_devices="$protected_devices $dev"
		done
	done

	rebuild_skipped_volumes
	protected_devices="${protected_devices## }"
	debug "Protect devices: ${protected_devices:-<EMPTY LIST>}"; debug
}

# Определяет все целевые носители, пригодные для бэкапа
#
autodetect_targets() {
	local devno devname sysfs f b plist

	debug "Auto-detecting targets..."

	while read devno devname; do
		if [ ! -b "/dev/$devname" ]; then
			debug "  - /dev/$devname is not a block device"
			continue
		fi
		if is_protected "$devname"; then
			debug "  - /dev/$devname protected directly"
			continue
		fi
		sysfs="$(readlink -fv "/sys/dev/block/$devno")"
		if [ ! -r "$sysfs/dev" ]; then
			debug "  - $devname not found in the /sys/dev/block"
			continue
		fi
		read b < "$sysfs/dev"
		if [ "$b" != "$devno" ]; then
			debug "  - /dev/$devname will be skipped ($devno<>$b)"
			continue
		fi
		[ -r "$sysfs/ro" ] && read b < "$sysfs/ro" ||
			b="$(blockdev --getro "/dev/$devname")"
		if [ "$b" != "0" ]; then
			debug "  - /dev/$devname is read-only, will be skipped"
			continue
		fi
		if [ $removable -eq 0 -a -r "$sysfs/removable" ]; then
			read b < "$sysfs/removable"
			if [ "$b" != "0" ]; then
				debug "  - /dev/$devname is removable, will be skipped"
				continue
			fi
		fi
		targets="$targets $devname"
	done < "$workdir"/devices

	[ -n "$targets" ] ||
		fatal "Целевых устройств не обнаружено!"
	targets="${targets## }"
}

# Определяет и выводит версию операционной системы
#
get_sysver() {
	local f="${1-}/etc/os-release"
	local version="Unknown" v=

	if [ -r "$f" ]; then
		if grep -qE '^PRETTY_NAME=' "$f"; then
			v="$(grep -sE '^PRETTY_NAME=' "$f" |tail -n1 |cut -f2- -d= |tr -d '"')"
			[ -z "$v" ] || version="$v"
		elif grep -qE '^NAME=' "$f" && grep -qE '^VERSION=' "$f"; then
			v="$(grep -sE '^NAME=' "$f" |tail -n1 |cut -f2- -d= |tr -d '"')"
			if [ -n "$v" ]; then
				version="$v"
				v="$(grep -sE '^VERSION=' "$f" |tail -n1 |cut -f2- -d= |tr -d '"')"
				[ -z "$v" ] || version="$version $v"
			fi
		fi
	fi

	echo -n "$version"
}

# Сканирует все целевые разделы (ищет корневые разделы Linux).
# В режиме scan-only все найденные корневые разделы выводятся
# сплошным списком, после чего работа скрипта прекращается.
# В остальных случаях проверяется, что найден хотя бы один
# корневой раздел, что найден лишь один корневой раздел либо
# найден раздел, соответвующий указанному порядковому номеру.
#
search_linux_rootfs() {
	local p fstype mp system n=0

	debug "Search Linux root partitions..."

	for p in $targets; do
		fstype="$(getfstype "/dev/$p")"
		if is_linux_fs "$fstype"; then
			debug "  - probing device /dev/$p ($fstype)..."
		else
			debug "  - skipping device /dev/$p ($fstype)..."
			continue
		fi
		mp="$(device_mpoint "$p" "$fstype")"
		if [ -d "$mp" ]; then
			mount --bind -- "$mp" "$probe_dir" \
				>/dev/null 2>&1 || fstype=
		else
			mount -t "$fstype" -o ro,$rootopts -- "/dev/$p" \
				"$probe_dir" >/dev/null 2>&1 || fstype=
		fi
		[ -n "$fstype" ] || continue
		if [ -r "$probe_dir/etc/fstab" ] &&
			[ -r "$probe_dir/etc/os-release" ] &&
			[ -d "$probe_dir/usr" -a -d "$probe_dir/bin" ] &&
			[ -d "$probe_dir/sbin" -a -d "$probe_dir/proc" ]
		then
			n=$(($n + 1))
			system="$(get_sysver "$probe_dir")"
			debug "    #$n found: $system"
			if [ $scan_only -ne 0 ]; then
				echo -e "$n. /dev/$p ($fstype):\t$system"
			else
				cat "$probe_dir/etc/fstab" | sed -e '\,[[:space:]]*#,d' \
					-e '\,^[[:space:]]\+$,d' -e 's,[[:space:]]\+,\t,g' |
				sort -k2,2 -so "$workdir/backup-fstab.$n"
#				[ $verbose -eq 0 ] || {
#					echo "RootFS #$n /etc/fstab:"
#					cat "$workdir/backup-fstab.$n"
#					echo
#				} >&2
			fi
		fi
		cond_unmount "$probe_dir"
	done

	[ $scan_only -eq 0 ] || exit 0
	[ $n -ne 0 ] || fatal "Корневой раздел Linux не найден!"
	if [ -n "$rootfs_number" -a "$rootfs_number" != "0" ]; then
		[ $n -ge $rootfs_number ] 2>/dev/null ||
			fatal "Указан неверный порядковый номер корневого раздела!"
	elif [ $n -eq 1 ]; then
		rootfs_number=1
	else
		fatal "Найдено более одного корневого раздела, нужно указать сохраняемый!"
	fi
}

# Пытается определить ID загрузочной записи UEFI
# в подмонтированной корневой файловой системе
#
setup_boot_loader() {
	local ev

	# С использованием файла /etc/os-release
	if [ -z "$boot_loader_id" ]; then
		ev="grep -sE \"^ID=\" -- \"$probe_dir/etc/os-release\""
		ev="$ev |tail -n1 |cut -c4- |tr -d '\"'"
		boot_loader_id="$(eval $ev)"
		[ -z "$boot_loader_id" ] ||
			debug "  - boot loader id: $boot_loader_id (from /etc/os-release)"
	fi

	# С использованием файла /etc/sysconfig/grub2
	if [ -z "$boot_loader_id" ]; then
		ev="$probe_dir/etc/sysconfig/grub2"
		if [ -r "$ev" ]; then
			ev="grep -sE \"^GRUB_BOOTLOADER_ID=\" -- \"$ev\""
			ev="$ev |tail -n1 |cut -f2- -d= |tr -d '\"'"
			boot_loader_id="$(eval $ev)"
			[ -z "$boot_loader_id" ] ||
				debug "  - boot loader id: $boot_loader_id (from /etc/sysconfig/grub2)"
		fi
	fi
}

# Анализирует ранее сохранённый /etc/fstab,
# соответствующий порядковому номеру rootfs
#
parse_single_fstab() {
	local dev mpoint fstype dummy opts mp

	debug "Analyzing /etc/fstab..."

	while read dev mpoint fstype dummy; do
		case "$fstype" in
		swap)	[ -z "$swappart" ] || {
				debug "  - one more SWAP partition will be ignored: $dev"
				continue
			};;
		vfat)	[ "$mpoint" = "/boot/efi" ] || {
				debug "  - foreign FAT partition will be ignored: $dev"
				continue
			};;
		*)	[ -n "$dev" -a -n "$fstype" ] ||
				continue
			is_linux_fs "$fstype" || {
				debug "  - this record will be ignored: $dev ($fstype)"
				continue
			};;
		esac

		debug "  - checking device $dev ($fstype)..."
		case "$dev" in
		LABEL=?*)   dev="$(blkid -L "${dev##LABEL=}" 2>/dev/null ||:)";;
		UUID=?*)    dev="$(blkid -U "${dev##UUID=}" 2>/dev/null ||:)";;
		esac
		[ -n "$dev" ] || continue
		dev="$(readlink -fv "$dev")"
		case "$dev" in
		/dev/?*)    ;;
		*)	    continue;;
		esac
		[ -b "$dev" ] || continue
		if in_array "$dev" $skip_volumes; then
			debug "  - device $dev ($fstype) must be skipped"
			continue
		elif in_array "$mpoint" $skip_volumes; then
			debug "  - volume $mpoint ($dev) must be skipped"
			continue
		elif [ "$fstype" = "swap" ]; then
			debug "  - first SWAP partition found: $dev"
			swappart="$dev"
			continue
		else
			[ "$fstype" = "vfat" ] &&
				opts="ro,$vfatopts" || opts="ro,$dataopts"
			debug "  - probing device $dev ($mpoint)..."
		fi
		mp="$(device_mpoint "$dev" "$fstype")"
		if [ -d "$mp" ]; then
			mount --bind -- "$mp" "$probe_dir" \
				>/dev/null 2>&1 || fstype=
		else
			mount -t "$fstype" -o $opts -- "$dev" \
				"$probe_dir" >/dev/null 2>&1 || fstype=
		fi
		if [ -z "$fstype" ]; then
			debug "  - error mounting partition $dev, skipping"
			continue
		fi

		case "$mpoint" in
		/)	[ "$fstype" != "vfat" ] &&
			[ -r "$probe_dir/etc/fstab" ] &&
			[ -r "$probe_dir/etc/os-release" ] &&
			[ -d "$probe_dir/usr" -a -d "$probe_dir/bin" ] &&
			[ -d "$probe_dir/sbin" -a -d "$probe_dir/proc" ] ||
				fatal "Ошибочная запись для раздела / (root)!"
			[ -z "$rootpart" ] ||
				fatal "Повторная запись для раздела / (root)!"
			debug "  - / (root) partition found: $dev ($fstype)"
			setup_boot_loader
			rootpart="$dev"
			;;

		/boot)	[ "$fstype" != "vfat" ] ||
				fatal "Ошибочная запись для раздела /boot!"
			[ "$(uname -m)" = "e2k" -a -f "$probe_dir/boot.conf" ] ||
			[ -d "$probe_dir/efi" -o -e "$probe_dir/vmlinuz" ] ||
			[ -d "$probe_dir/lilo" -o -d "$probe_dir/extlinux" ] ||
			[ -d "$probe_dir/grub" ] ||
				fatal "Ошибочная запись для раздела /boot!"
			[ -z "$bootpart" ] ||
				fatal "Повторная запись для раздела /boot!"
			debug "  - /boot partition found: $dev ($fstype)"
			bootpart="$dev"
			;;

		/boot/efi)
			[ "$fstype" = "vfat" ] &&
			[ -d "$probe_dir/EFI" -o -d "$probe_dir/efi" ] ||
				fatal "Ошибочная запись для раздела /boot/efi!"
			[ -z "$esp_part" ] ||
				fatal "Повторная запись для раздела /boot/efi!"
			debug "  - /boot/efi (ESP) partition found: $dev (vfat)"
			esp_part="$dev"
			;;

		/var)	[ "$fstype" != "vfat" -a -d "$probe_dir/log" ] &&
			[ -d "$probe_dir/db" -a -d "$probe_dir/cache" ] ||
				fatal "Ошибочная запись для раздела /var!"
			[ -z "$var_part" ] ||
				fatal "Повторная запись для раздела /var!"
			debug "  - /var partition found: $dev ($fstype)"
			var_part="$dev"
			;;

		/home)	[ "$fstype" != "vfat" ] &&
			[ -n "$(glob "$probe_dir/*/.bash_profile")" ] ||
				fatal "Ошибочная запись для раздела /home!"
			[ -z "$homepart" ] ||
				fatal "Повторная запись для раздела /home!"
			debug "  - /home partition found: $dev ($fstype)"
			homepart="$dev"
			;;

		/mnt/?*|/srv|/srv/?*|/usr|/usr/?*|/var/?*)
			[ "$fstype" != "vfat" ] ||
				fatal "Ошибочная запись для раздела $mpoint!"
			in_array "$dev:$fstype:$mpoint" $other_parts &&
				fatal "Повторная запись для раздела $mpoint!" ||:
			debug "  - $mpoint partition found: $dev ($fstype)"
			other_parts="$other_parts $dev:$fstype:$mpoint"
			;;

		*)	debug "  - Non-FHS partition will be skipped: $mpoint ($dev)"
			;;
		esac
		cond_unmount "$probe_dir"
	done < "$workdir/backup-fstab.$rootfs_number"

	[ -n "$rootpart" ] ||
		fatal "Корневой раздел (/) не найден в /etc/fstab!"
	other_parts="${other_parts## }"
}

# Ищет целые диски, на которых находятся сохраняемые разделы
#
search_whole_disks() {
	local device partdev wholedev number check

	debug "Search whole disk drives..."; targets=

	for device in $rootpart $esp_part $bootpart \
		$swappart $var_part $homepart $other_parts
	do
		partdev="${device##/dev/}"
		[ -n "$partdev" ] || continue
		[ -b "/dev/$partdev" ] || continue
		[ ! -f "/sys/block/$partdev/dev" ] || continue
		[ -f "/sys/class/block/$partdev/dev" ] || continue
		[ -f "/sys/class/block/$partdev/partition" ] || continue
		read number < "/sys/class/block/$partdev/partition"
		wholedev="${partdev%$number}"
		[ -b "/dev/$wholedev" ] || wholedev="${partdev%p}"
		[ -b "/dev/$wholedev" ] || continue
		wholedev="$(readlink -fv "/dev/$wholedev")"
		case "$wholedev" in
		/dev/*)	wholedev="${wholedev##/dev/}";;
		*)	continue;;
		esac
		in_array "$wholedev" $targets && continue ||:
		[ -f "/sys/block/$wholedev/$partdev/dev" ] || continue
		[ -f "/sys/block/$wholedev/$partdev/partition" ] || continue
		read check < "/sys/block/$wholedev/$partdev/partition"
		[ "$number" = "$check" ] || continue
		read number < "/sys/class/block/$partdev/dev"
		read check < "/sys/block/$wholedev/$partdev/dev"
		[ "$number" = "$check" ] || continue
		debug "  - /dev/$wholedev found"
		targets="$targets $wholedev"
	done

	if [ -z "$targets" ]; then
		number="$(ls -X /sys/block 2>/dev/null ||:)"
		if [ -n "$number" ]; then
			for device in $number; do
				if [ -n "$skip_disk_names" ]
				then
					check=0
					for partdev in $skip_disk_names; do
						case "$device" in
						${partdev}[0-9]*)
							check=1
							break
							;;
						esac
					done
					[ $check -eq 0 ] || continue
				fi

				wholedev="/dev/$device"
				partdev="/sys/block/$device"
				[ -b "$wholedev" ] || continue
				[ -f "$partdev/dev" ] || continue
				in_array "$device" $targets && continue ||:
				[ -r "$partdev/ro" ] && read check < "$partdev/ro" ||
					check="$(blockdev --getro "$wholedev")"
				if [ "$check" != "0" ]; then
					debug "  - $wholedev is read-only, will be skipped"
					continue
				fi
				if [ $removable -eq 0 -a -r "$partdev/removable" ]; then
					read check < "$partdev/removable"
					if [ "$check" != "0" ]; then
						debug "  - $wholedev is removable, will be skipped"
						continue
					fi
				fi
				check="$(blkid -o value -s PTTYPE "$wholedev")"
				if [ "$check" != "dos" -a "$check" != "gpt" ]; then
					debug "  - $wholedev has no MBR/GPT label, will be skipped"
					continue
				fi

				if is_protected "$device"; then
					debug "  - $wholedev protected directry"
				else
					debug "  - $wholedev found"
					targets="$targets $device"
				fi
			done
		fi
	fi

	targets="${targets## }"
	debug "Whole disks:     ${targets:-<NOT FOUND>}"
}

# Монтирует целевой раздел и определяет объём занятого пространства
#
# $1=device	Монтируемый раздел, например, "/dev/sda2"
# $2=fstype	Файловая система, если известна, например, "ext4"
# $3=mpoint	Относительная точка монтирования, например, "/boot"
# $4=mntopts	Опции монтирования раздела, например, "nodev,nosuid"
#
mount_target() {
	local device="$1" fstype="$2" x="-xs -B512" i usage archive
	[ -n "$fstype" ] || fstype="$(getfstype "$device")"
	local mp="$(device_mpoint "$device" "$fstype")"
	local mpoint="$3" options="$access,$4"
	local target="/mnt/target$mpoint"

	[ -d "$target" ] ||
		fatal "Точка монтирования $target не найдена!"
	if [ -z "$mp" ]; then
		mount -t "$fstype" -o "$options" -- "$device" "$target"
	elif [ $cleanup -ne 0 ]; then
		fatal "Текущую файловую систему очищать нельзя!"
	elif [ $rename_iface -ne 0 ]; then
		fatal "В текущей файловой системе переименовывать интерфейс нельзя!"
	else
		mount --bind -- "$mp" "$target"
	fi
	for i in $excludes; do
		x="$x --exclude='$i'"
	done
	usage=$(du $x "$target" 2>/dev/null |tail -n1 |cut -f1)
	[ -n "$usage" ] && usage=$(($usage * 512)) || usage=0
	archive="$(echo -n "${mpoint:-root}" |sed -e 's,[/_\-],,g' |
					cut -c1-$archive_name_limit)"
	[ "$archive" != "bootefi" ] || archive="esp"
	echo -n "$usage" > "$metadir/$archive.used"
	usage=$(size2mib ${usage:-0})
	debug "  - ${mpoint:-/ (root)} mounted: $device (${usage}MiB used)"
	volumes="${volumes-} $device:$fstype:${mpoint:-/}"
}

# Чистит /home
#
cleanup_home() {
	local entry item

	debug "Cleanup /home..."
	find . -maxdepth 1 -type d -and ! -name '.' |
		cut -c3- > "$workdir/CLEANUP.HOME"

	while read entry; do
		[ "$entry" != 'lost+found' ] ||
			continue
		[ -d "$entry" ] ||
			continue
		[ -f "$entry/.bash_profile" ] ||
		[ -f "$entry/.bash_logout" ] ||
		[ -f "$entry/.bashrc" ] ||
			continue
		debug "  - User found: '$entry'"
		(	set +e; set +f
			cd "$entry"
			[ -z "$(glob '.xsession-errors*')" ] ||
				run rm -f -- .xsession-errors*
			for item in cache dbus local linuxmint config/caja \
				config/gconf config/goa-1.0 config/menus \
				config/mintmenu config/parcellite \
				config/pulse xsession.d apt
			do
				[ ! -d ".$item" ] || run rm -rf -- ".$item"
			done
			for item in config/Trolltech.conf config/monitors.xml \
				ICEauthority bash_history ssh/agent
			do
				[ ! -e ".$item" ] || run rm -f -- ".$item"
			done
		)
	done < "$workdir/CLEANUP.HOME"
}

# Чистит /var/log
#
cleanup_logs() {
	debug "Cleanup /var/log..."
	(	set +e; set +f
		local entry
		[ -z "$(glob 'journal/???*')" ] ||
			run rm -rf -- journal/???*
		run find . -type f -name '*.old' -delete
		[ -z "$(glob Xorg.0.log alterator-net-iptables rpmpkgs)" ] ||
			run rm -f -- Xorg.0.log alterator-net-iptables rpmpkgs
		find . -type f -and ! -empty |cut -c3- |grep -vE '^README$' |
		while read entry; do
			debug "Wiping '$entry'..."
			[ $dryrun -ne 0 ] || :> "$entry"
		done
	)
}

# Чистит /var
#
cleanup_var() {
	debug "Cleanup /var..."
	(	set +e; set +f
		local entry
		[ ! -f lib/openvpn/etc/resolv.conf ] ||
			run sed -i '/^nameserver /d' lib/openvpn/etc/resolv.conf
		[ ! -f resolv/etc/resolv.conf ] ||
			run sed -i '/^nameserver /d' resolv/etc/resolv.conf
		[ ! -d lib/ldm/.dbus/session-bus ] ||
			run rm -rf -- lib/ldm/.dbus/session-bus
		[ -z "$(glob 'cache/fontconfig/*.cache-?*')" ] ||
			run rm -f -- cache/fontconfig/*.cache-?*
		[ -z "$(glob 'lib/NetworkManager/dhclient-*.lease')" ] ||
			run rm -f -- lib/NetworkManager/dhclient-*.lease
		[ -z "$(glob 'lib/NetworkManager/dhclient-*.conf')" ] ||
			run rm -f -- lib/NetworkManager/dhclient-*.conf
		[ -z "$(glob 'lib/dhcpcd/*.lease')" ] ||
			run rm -f -- lib/dhcpcd/*.lease
		for entry in lib/NetworkManager/timestamps lib/dbus/machine-id \
			run/alteratord/alteratord.log run/avahi-daemon/pid \
			run/alteratord.pid run/cupsd.pid lib/random-seed \
			lib/systemd/random-seed
		do
			[ ! -f "$entry" ] || run rm -f -- "$entry"
		done
	)
}

# Чистит /boot
#
cleanup_boot() {
	debug "Cleanup /boot..."
	(	set +e; set +f
		[ -z "$(glob 'initrd-[2-9]*.img')" ] ||
			run rm -f -- initrd-[2-9]*.img
		[ ! -f grub/grub.cfg ] ||
			run rm -f -- grub/grub.cfg
		[ ! -f grub/grubenv ] ||
			run rm -f -- grub/grubenv
	)
}

# Чистит корень
#
cleanup_root() {
	debug "Cleanup / (root)..."
	(	set +e; set +f
		local m kernel
		[ ! -f etc/resolv.conf ] ||
			run sed -i '/^nameserver /d' etc/resolv.conf
		[ -z "$(glob 'etc/openssh/ssh_host_*key*')" ] ||
			run rm -f etc/openssh/ssh_host_*key*
		[ -z "$(glob 'etc/udev/rules.d/*persistent*.rules')" ] ||
			run rm -f etc/udev/rules.d/*persistent*.rules
		[ -z "$(glob 'run/blkid/blkid*')" ] ||
			run rm -f run/blkid/blkid*
		[ -z "$(glob 'tmp/.private/*')" ] ||
			run rm -rf tmp/.private/*
		[ -z "$(glob 'etc/*.bak')" ] ||
			run rm -f etc/*.bak
		[ -z "$(glob 'etc/*.old')" ] ||
			run rm -f etc/*.old
		[ ! -d root/.cache ] ||
			run rm -rf root/.cache
		[ ! -d root/.loacl ] ||
			run rm -rf root/.local
		[ ! -d tmp/alterator ] ||
			run rm -rf tmp/alterator
		[ ! -d tmp/hsperfdata_root ] ||
			run rm -rf tmp/hsperfdata_root
		[ ! -f etc/resolv.conf.dnsmasq ] ||
			run rm -f etc/resolv.conf.dnsmasq
		[ ! -f etc/firsttime.flag ] ||
			run rm -f etc/firsttime.flag
		[ ! -f etc/machine-id ] ||
			run rm -f etc/machine-id
		[ ! -f root/.bash_history ] ||
			run rm -f root/.bash_history
		[ ! -f run/messagebus.pid ] ||
			run rm -f run/messagebus.pid
		for kernel in lib/modules/*; do
			[ "$kernel" != 'lib/modules/*' ] || break
			m="$kernel/kernel/drivers/block"
			[ ! -d "$m/drbd" -a ! -d "$m/zram" ] || continue
			run rm -rf -- "$kernel"
		done
	)
}

# Переименовывает единственный сетевой интерфейс в eth0
#
rename_network_iface() {
	local src= i= n=0
	local ifaces="$(ls -X etc/net/ifaces/ 2>/dev/null ||:)"

	if [ -z "$ifaces" ]; then
		debug "WARNING: No network interfaces found for renaming"
		return 0
	fi

	for i in $ifaces; do
		case "$i" in
		default|lo|unknown)
			continue
			;;
		wlan[0-9]*)
			continue
			;;
		*)	[ -d "etc/net/ifaces/$i" ] ||
				continue
			n=$(($n + 1))
			src="$i"
			;;
		esac
	done

	if [ $n -eq 1 -a -n "$src" -a "$src" != "eth0" ]; then
		debug "Renaming network interface '$src' => 'eth0'..."
		run mv -f -- "etc/net/ifaces/$src" "etc/net/ifaces/eth0"
	elif [ $n -gt 1 ]; then
		debug "WARNING: $n network interfaces found, skip renaming"
	else
		debug "WARNING: No network interfaces found for renaming"
	fi
}

# Убеждается, что переданный после опции $1 параметр $2 не пуст.
# Иначе выводит сообщение $3, подсказку и завершает работу.
#
not_empty() {
	[ -n "$2" ] || {
		echo "После '$1' следует указывать $3."
		show_help 1
	} >&2
}

# Убеждается, что целевой диск ещё не был определён.
# Иначе выводит сообщение, подсказку и завершает работу.
#
tgt_empty() {
	[ -z "$targets" ] || {
		echo "Целевой диск можно определить только один раз!"
		show_help 1
	} >&2
}

# Разбор аргументов командной строки
#
parse_args() {
	local dummy
	run_args="$*"
	local s_opts="+b:,n:,s:,A:,t:,l:,f:,U:,L:,X:,C,S,N,r,R,p,c,v,q,h"
	local   opts="base:,name:,skip:,tar:,loader:,fstab:,rootfs:"
		opts="$opts,uuid:,label:,current,scan-only,scanonly"
		opts="$opts,no-act,dry-run,dryrun,removable,no-nvram"
		opts="$opts,nonvram,clean,cleanup,verbose,quiet,algo:"
		opts="$opts,checksum:,checksums:,self,rename-iface"
		opts="$opts,exclude:,help"

	opts=$(getopt -n "$progname" -o $s_opts -l $opts -- "$@") ||
		show_help 1 >&2
	eval set -- "$opts"
	while [ $# -gt 0 ]; do
		case "$1" in
		-b|--base)
			not_empty "$1" "${2-}" "путь к хранилищу бэкапов"
			backup_base="$2"
			shift
			;;
		-n|--name)
			not_empty "$1" "${2-}" "название создаваемого бэкапа"
			backup_name="$2"
			shift
			;;
		-s|--skip)
			not_empty "$1" "${2-}" "пропускаемые разделы"
			skip_volumes="$skip_volumes $(echo -n "$2" |tr ',' ' ')"
			shift
			;;
		-A|--algo|--checksum|--checksums)
			not_empty "$1" "${2-}" "алгоритмы контрольных сумм"
			opts="$(echo -n "$2" |tr ',' ' ' |tr '[[:upper:]]' '[[:lower:]]')"
			digests=
			for dummy in $opts; do
				case "$dummy" in
				md5|sha1|sha256)
					digests="$digests $dummy"
					;;
				none)	digests=""; break
					;;
				*)	fatal "Неподдерживаемый алгоритм: '$dummy'!"
					;;
				esac
			done
			digests="${digests## }"
			shift
			;;
		-t|--tar)
			not_empty "$1" "${2-}" "дополнительные опции tar"
			for dummy in $(echo -n "$2" |tr ',' ' '); do
				tar_opts="$tar_opts --$dummy"
			done
			shift
			;;
		-l|--loader)
			not_empty "$1" "${2-}" "ID записи UEFI-загрузки"
			boot_loader_id="$2"
			shift
			;;
		-f|--fstab|--rootfs)
			not_empty "$1" "${2-}" "порядковый номер RootFS"
			rootfs_number="$2"
			shift
			;;
		-U|--uuid)
			not_empty "$1" "${2-}" "UUID корневого раздела"
			tgt_empty
			tgtway="uuid"
			targets="$2"
			shift
			;;
		-L|--label)
			not_empty "$1" "${2-}" "LABEL корневого раздела"
			tgt_empty
			tgtway="label"
			targets="$2"
			shift
			;;
		-X|--exclude)
			not_empty "$1" "${2-}" "шаблон исключаемых файлов"
			excludes="$excludes $2"
			shift
			;;
		-C|--current|--self)
			tgt_empty
			targets="/"
			tgtway="root"
			;;
		-S|--scan-only|--scanonly)
			scan_only=1
			;;
		-N|--no-act|--dry-run|--dryrun)
			dryrun=1
			;;
		-r|--removable)
			removable=1
			;;
		-R|--rename-iface)
			rename_iface=1
			;;
		-p|--no-nvram|--nonvram)
			no_nvram=1
			;;
		-c|--clean|--cleanup)
			cleanup=1
			;;
		-v|--verbose)
			verbose=1
			;;
		-q|--quiet)
			quiet=1
			;;
		-h|--help)
			show_help
			;;
		--)	shift
			[ -z "${1-}" ] || tgt_empty
			break
			;;
		-*)	echo "Неподдерживаемая опция: '$1'" >&2
			show_help 1 >&2
			;;
		*)	tgt_empty
			break
			;;
		esac
		shift
	done

	[ $# -le 1 ] ||
		fatal "Слишком много аргументов, да поможет вам --help!"
	[ -n "$targets" ] || targets="${1-}"
}


# Начало
parse_args "$@"
debug "Checking backup conditions..."

# Проверяем привелегии
[ "$(id -u)" = "0" ] || fatal "Вы должны быть root для запуска этой программы!"

# Создаём рабочий каталог
export TMPDIR="${TMPDIR-/tmp}"
workdir="$(mktemp -dt "$progname-XXXXXXXX.tmp")"
chmod 755 "$workdir"

# Устанавливаем обработчик выхода из скрипта
trap exit_handler HUP PIPE INT QUIT TERM EXIT

# Создаём каталог метаданных и временную точку монтирования
metadir="$workdir/metadata"
probe_dir="$workdir/probe"
mkdir -m755 -- "$metadir"
mkdir -m755 -- "$probe_dir"

# Показываем кэш blkid
if [ $verbose -ne 0 ]; then
	debug; debug "BLKID cache:"
	blkid >&2
	debug
fi

# Создаём карту всех блочных устройств
show_devmap > "$workdir"/devices
if [ $verbose -ne 0 ]; then
	debug "Devices MAP:"
	cat "$workdir"/devices >&2
	debug
fi

# Проверяем указанное целевое устройство либо пытаемся его обнаружить
case "$tgtway" in
root)	targets="$(awk '{print $1 " " $2;}' /proc/mounts 2>/dev/null |
			grep -sE ' \/$' |tail -n1 |cut -f1 -d ' ')" ||
		fatal "Вероятно, файловая система /proc не смонтирована!"
	;;
label)	targets="$(blkid -L "$targets" 2>/dev/null)" ||
		fatal "Указана неверная метка тома либо такого тома не существует!"
	;;
uuid)	targets="$(blkid -U "$targets" 2>/dev/null)" ||
		fatal "Указан неверный UUID либо такого раздела не существует!"
	;;
esac
if [ -n "$targets" ]; then
	check_target_device
	rebuild_skipped_volumes
else
	skip_volumes="$skip_volumes LABEL=alt-autorun"
	autodetect_protected
	autodetect_targets
	tgtway="auto"
fi

# Загружаем модули ядра
debug "Loading kernel modules..."
for module in ext4 jfs xfs reiserfs btrfs zfs vfat; do
	grep -qws "$module" /proc/modules ||
		modprobe $module >/dev/null 2>&1 && debug "  - $module" ||:
done
debug
unset module

# Промежуточная диагностика
if [ $verbose -ne 0 ]; then
	debug "Current time:    $(LC_TIME=C date "+%F %T")"
	debug "Exec arguments:  $run_args"
	debug "Work directory:  $workdir"
	debug "Backup base:     $backup_base"
	debug "Backup name:     $backup_name"
	debug "Kernel version:  $(uname -rom)"
	debug "System version:  $(get_sysver)"
	debug "Script run mode: $(test $dryrun -eq 0 && echo NORMAL || echo SIMULATION)"
	debug "Removable disks: $(test $removable -eq 0 && echo DISABLED || echo ENABLED)"
	debug "Write to NVRAM:  $(test $no_nvram -ne 0 && echo DISABLED || echo ENABLED)"
	debug "Cleanup first:   $(test $cleanup -eq 0 && echo DISABLED || echo ENABLED)"
	debug "RootFS number:   ${rootfs_number:-<NOT DEFINED>}"
	debug "Checksum types:  ${digests:-<EMPTY LIST>}"
	debug "Target devices:  $targets"
	debug "TGT detection:   $tgtway"
	debug
fi

# Сканируем разделы: ищем все потенциальные rootfs
search_linux_rootfs; debug

# Анализируем ранее сохранённый файл /etc/fstab
parse_single_fstab; debug

# Определяем диски, на которых находятся сохраняемые разделы
search_whole_disks; debug

# Проверяем разделы на отсутствие противоречий
[ -n "$rootpart" ] && must_be_linux "$rootpart" / ||
	fatal "Корневой раздел (/) должен быть определён!"
[ -z "$bootpart" ] || must_be_linux "$bootpart" /boot
[ -z "$var_part" ] || must_be_linux "$var_part" /var
[ -z "$homepart" ] || must_be_linux "$homepart" /home
if [ -n "$esp_part" ]; then
	expect_fs "$esp_part" vfat ||
		fatal "Ошибочный раздел ESP: $esp_part!"
fi
if [ -n "$swappart" ]; then
	expect_fs "$swappart" swap ||
		fatal "Ошибочный раздел SWAP: $swappart!"
fi
if [ -n "$other_parts" ]; then
	for record in $other_parts; do
		device="$(echo -n "$record" |cut -f1 -d:)"
		mpoint="$(echo -n "$record" |cut -f3- -d:)"
		must_be_linux "$device" "$mpoint"
	done
	unset record device mpoint
fi

# Бэкап должен быть доступен только пользователю root
umask 0077

# Размонтируем целевые разделы
unmount_all_targets

# Снова монтируем целевые разделы
debug "Mounting target filesystems..."
[ -d /mnt/target ] || mkdir -m755 /mnt/target
[ $cleanup -eq 0 -a $rename_iface -eq 0 ] && access="ro" || access="rw"
mount_target "$rootpart" "" "" "$rootopts"
[ -z "$bootpart" ] || mount_target "$bootpart" "" /boot "$bootopts"
[ -z "$esp_part" ] || mount_target "$esp_part" vfat /boot/efi "$vfatopts"
[ -z "$var_part" ] || mount_target "$var_part" "" /var "$dataopts"
[ -z "$homepart" ] || mount_target "$homepart" "" /home "$dataopts"
if [ -n "$other_parts" ]; then
	for record in $other_parts; do
		device="$(echo -n "$record" |cut -f1 -d:)"
		fstype="$(echo -n "$record" |cut -f2 -d:)"
		mpoint="$(echo -n "$record" |cut -f3- -d:)"
		case "$mpoint" in
		/usr|/usr/?*)	options="$rootopts";;
		*)		options="$dataopts";;
		esac
		mount_target "$device" "$fstype" "$mpoint" "$options"
	done
	unset record device fstype mpoint options
fi
debug
unset access
volumes="${volumes## }"

# Хранилище бэкапов к этому моменту должно быть доступно
[ -d "$backup_base" ] || fatal "Хранилище бэкапов недоступно: $backup_base!"

# А в случае Rescue Launcher, оно должно быть смонтировано
if [ "$backup_base" = "/mnt/autorun" ]; then
	if grep -qs " /mnt/autorun ext2 ro," /proc/mounts; then
		debug "Remounting /mnt/autorun for R/W-access..."
		mount -o remount,rw /mnt/autorun
	elif ! grep -qs " /mnt/autorun ext2 rw," /proc/mounts; then
		debug "Mounting /mnt/autorun (alt-autorun) media..."
		mount -t ext2 -o rw,nodev,nosuid -L alt-autorun /mnt/autorun
		need_unmount=1
	fi
fi

# Исключаем точку монирования самого хранилища бэкапов
in_array "${backup_base:1}" $excludes || excludes="$excludes ${backup_base:1}"

# Каталог очередного бэкапа будет создан в процессе
backupdir="$(realpath "$backup_base")/$backup_name"
[ ! -d "$backupdir" ] || fatal "Резервная копия '$backup_name' уже существует!"
debug "Backup FullPath: $backupdir"

# Создаём всю структуру подкаталогов для очередного бэкапа
run mkdir -pm700 -- "$backupdir"; debug
:> "$metadir/LOADERS"

# Сохраняем архитектуру целевого хоста
echo -n "$(uname -m)" > "$metadir"/ARCH
dummy=

# Сохраняем названия загрузчиков
[ "$(uname -m)" != "e2k" -o ! -f /mnt/target/boot/boot.conf ] ||
	echo "elbrus" >> "$metadir/LOADERS"
[ ! -d /mnt/target/boot/grub ] ||
	echo "grub" >> "$metadir/LOADERS"
[ ! -d /mnt/target/boot/lilo ] ||
	echo "lilo" >> "$metadir/LOADERS"
[ ! -d /mnt/target/boot/extlinux ] ||
	echo "extlinux" >> "$metadir/LOADERS"

# Сохраняем путь к файлу random-seed
if [ -f /mnt/target/var/lib/systemd/random-seed ]; then
	echo -n "/var/lib/systemd/random-seed" > "$metadir/RNDSEED"
elif [ -f /mnt/target/var/lib/random-seed ]; then
	echo -n "/var/lib/random-seed" > "$metadir/RNDSEED"
fi

# Определяем имя целевого хоста
net="/mnt/target/etc/sysconfig/network"
if [ -r "$net" ]; then
	grep -qE "^HOSTNAME=" "$net" &&
		dummy="$(grep -sE "^HOSTNAME=" "$net" |
			cut -f2- -d= |tr -d "'" |tr -d '"')" ||:
elif [ -r /mnt/target/etc/HOSTNAME ]; then
	read dummy < /mnt/target/etc/HOSTNAME
elif [ -r /mnt/target/etc/hostname ]; then
	read dummy < /mnt/target/etc/hostname
fi

# Сохраняем имя хоста, /etc/fstab и версию выпуска ОС
[ -z "$dummy" ] || echo -n "$dummy" > "$metadir"/ORGHOST
cat /mnt/target/etc/fstab > "$metadir"/FSTABLE
get_sysver /mnt/target > "$metadir"/RELEASE
unset dummy net

# Формируем базовый список исключений для бэкапа корневого раздела
cat >"$workdir/EXCLUDES" <<-EOF
proc/*
sys/*
root/tmp/*
tmp/.private/*
EOF

# Сохраняем вложенные разделы
if [ -n "$other_parts" ]; then
	for record in $(echo -n "$other_parts" |tr ' ' '\n' |tac |tr '\n' ' '); do
		device="$(echo -n "$record" |cut -f1 -d:)"
		mpoint="$(echo -n "$record" |cut -f3- -d:)"
		cd "/mnt/target$mpoint/"
		[ "$mpoint" != "/var/log" ] || [ $cleanup -eq 0 ] ||
			cleanup_logs
		archive="$(echo -n "$mpoint" |sed -e 's,[/_\-],,g' |
					cut -c1-$archive_name_limit)"
		[ ! -f "$backupdir/$archive.tgz" ] ||
			fatal "Архив с таким именем уже создан: $archive.tgz!"
		backup_linux "$archive" "$mpoint" .
		echo "${mpoint:1}/*" >> "$workdir/EXCLUDES"
		cd / && cond_unmount "/mnt/target$mpoint"
		run save_fsmeta "$device" "$archive"
		debug
	done
	unset record device mpoint archive
fi

# Чистим и сохраняем /var
if [ -n "$var_part" ]; then
	cd /mnt/target/var/
	if [ $cleanup -ne 0 ]; then
		echo -n "$other_parts" |tr ' ' '\n' |grep -qE ':\/var\/log$' || {
			cd log/ && cleanup_logs
			cd /mnt/target/var/
		}
		cleanup_var
	fi
	backup_linux var /var .
	echo "var/*" >> "$workdir/EXCLUDES"
	cd / && cond_unmount /mnt/target/var
	run save_fsmeta "$var_part" var
	debug
fi

# Чистим и сохраняем /home
if [ -n "$homepart" ]; then
	cd /mnt/target/home/
	[ $cleanup -eq 0 ] || cleanup_home
	backup_linux home /home .
	echo "home/*" >> "$workdir/EXCLUDES"
	cd / && cond_unmount /mnt/target/home
	run save_fsmeta "$homepart" home
	debug
fi

# Сохраняем /boot/efi
if [ -n "$esp_part" ]; then
	cd /mnt/target/boot/efi/
	backup_linux esp /boot/efi .
	[ -n "$bootpart" ] || echo "boot/efi/*" >> "$workdir/EXCLUDES"
	cd / && cond_unmount /mnt/target/boot/efi
	run save_fsmeta "$esp_part" esp
	debug
fi

# Чистим и сохраняем /boot
if [ -n "$bootpart" ]; then
	cd /mnt/target/boot/
	[ $cleanup -eq 0 ] || cleanup_boot
	backup_linux boot /boot "--exclude='efi/*'" .
	echo "boot/*" >> "$workdir/EXCLUDES"
	cd / && cond_unmount /mnt/target/boot
	run save_fsmeta "$bootpart" boot
	debug
fi

# Чистим корень
cd /mnt/target/
if [ $cleanup -ne 0 ]; then
	echo -n "$other_parts" |tr ' ' '\n' |grep -qE ':\/var\/log$' || {
		cd var/log/ && cleanup_logs
		cd /mnt/target/
	}
	[ -n "$var_part" ] || {
		cd var/ && cleanup_var
		cd /mnt/target/
	}
	[ -n "$homepart" ] || {
		cd home/ && cleanup_home
		cd /mnt/target/
	}
	[ -n "$bootpart" ] || {
		cd boot/ && cleanup_boot
		cd /mnt/target/
	}
	cleanup_root
fi

# Если определено, переименовываем интерфейс
[ $rename_iface -eq 0 ] || rename_network_iface
[ $cleanup -eq 0 -a $rename_iface -eq 0 ] || debug

# Отладка исключений
[ $verbose -eq 0 ] || {
	echo "Root partition backup excludes:"
	cat "$workdir/EXCLUDES"; echo
} >&2

# Сохраняем корень
backup_linux root "/ (корень)" -X "$workdir/EXCLUDES" .
cd / && cond_unmount /mnt/target
run save_fsmeta "$rootpart" root
debug

# Сохраняем информацию о разделе SWAP
if [ -n "$swappart" ]; then
	run save_fsmeta "$swappart" swap
	debug
fi

# Считаем контрольные суммы образов
[ $dryrun -ne 0 ] || cd "$backupdir"
if [ -n "$digests" ]; then
	pv="$(command -v pv 2>/dev/null ||:)"
	for algo in $digests; do
		command -v "${algo}sum" >/dev/null 2>&1 || continue
		dummy="$(echo "$algo" |tr '[[:lower:]]' '[[:upper:]]')"
		msg "Вычисляются контрольные суммы $dummy..."
		case "$algo" in
		sha1)	dummy="SHA";;
		sha256)	dummy="256";;
		esac
		debug "Executing: ${algo}sum *.tgz > checksum.$dummy"

		[ $dryrun -eq 0 ] || continue

		if [ $verbose -ne 0 -o $quiet -ne 0 -o ! -x "$pv" ]; then
			"${algo}sum" $(glob '*.tgz') > "$metadir/checksum.$dummy"
		else
			:> "$metadir/checksum.$dummy"
			for filename in $(glob '*.tgz'); do
				"$pv" -N "${filename%.tgz}" "$filename" |"${algo}sum" |
					awk '{print $1 "  '"$filename"'";}' >> "$metadir/checksum.$dummy"
			done
			unset filename
		fi
	done
	unset pv algo dummy
fi

# Определяем утилиту для работы с NVRAM и сохраняем запись UEFI-загрузки
if grep -qs ' /sys/firmware/efi/efivars efivarfs ' /proc/mounts; then
	mngr="$(command -v efibootmgr 2>/dev/null ||:)"
	[ $no_nvram -eq 0 -a -x "$mngr" ] || mngr=
else
	mngr=
fi
if [ -n "$mngr" ]; then
	[ -n "$boot_loader_id" ] || boot_loader_id="$default_loader_id"
	mngr="$("$mngr" -v 2>&1 |grep -E '^Boot[0-9]*' |grep -ws "$boot_loader_id" |head -n1)"
	if [ -n "$mngr" ]; then
		debug "UEFI-record found: $mngr"
		[ $dryrun -ne 0 ] || {
			echo -n "$boot_loader_id=";
			echo "$mngr" |awk '{print $3;}'
		} > "$metadir/EFIBOOT"
	fi
fi
unset mngr

# Сохраняем информацию о текущей схеме
# разметки диска в случае её доступности
#
if [ -n "$targets" ]; then
	echo -n "$targets" > "$metadir/TARGETS"
	for devname in $targets; do
		sfdisk -d "/dev/$devname" > "$metadir/$devname.sfdisk"
		blkid $(glob "/dev/$devname*")
	done > "$metadir/blkid.tab"
	unset devname
fi

# Завершающая стадия
debug "Finalizing backup..."
echo -n "$volumes" |tr ' ' '\n' > "$metadir/VOLUMES"
[ $no_nvram -eq 0 ] || :> "$metadir/NONVRAM"
echo -n "tgz" > "$metadir/ZIPTYPE"
echo -n "0.1" > "$metadir/VERSION"
if [ $dryrun -ne 0 ]; then
	[ $quiet -ne 0 ] || ls -lX1 -- "$metadir/"
else
	cd "$metadir"
	run tar -cpzf "$backupdir/META.tgz" \
		--numeric-owner $(glob '*')
	cd "$backupdir"
	run sync
	if [ $quiet -eq 0 ]; then
		echo; ls -lX1; echo
		tar -tzf META.tgz; echo; echo -n "Всего: "
		du -sh --apparent-size . |tail -n1 |awk '{print $1}'
	fi
fi
cd /
msg
msg "Резервная копия '$backup_name' успешно создана!"
exit 0

