LINUX.ORG.RU

Инкрементальный бэкап

 ,


2

4

Суть такова.
Нужно что-то, работающее по принципу tar --listed-incremental, т.е. создаётся state-файл для текущего состояния, всё архивируется, архив сохраняется. Когда нужно сделать инкрементальный бэкап, достаточно этого state-файла, чтобы определить, какие файлы добавились, изменились, архивируется дельта, состояние обновляется.
Чем не подходит tar. Во-первых, его состояние не хранит никаких атрибутов файлов, только время последней архивации, и сравнение всего происходит с этим временем (если часы сбились назад, всё сломается). Хотелось бы что-то типа того, как идёт первое сравнение в rsync, чтобы сохранялись атрибуты (время и размер) каждого файла, и проверка проводилась на строгое равенство. Во-вторых, оно привязывается к некому номеру device (можно отключить --no-check-device) и главное, к номерам inode директорий, вот это вот вообще не нужно!

Есть ли альтернативы?

Ответ на: комментарий от crutch_master
git: «pack» не является командой git. Смотрите «git --help».

The most similar command is
        repack


И вообще, поясни подробнее. Гит же хранит все данные (да ещё всех коммитов) у себя в .git, в принципе, чем это будет лучше какого-нибудь rdiff-backup?

TheAnonymous ★★★★★
() автор топика
Ответ на: комментарий от legolegs

Ещё раз, суть.
Бэкапы находятся оффлайн, например, записаны на болванки. Надо, чтобы при создании каждого нового инкрементального бэкапа, не приходилось их каждый раз теребить.
В принципе, задача не сложная - надо держать некоторый «индекс» (состояние) того, что было записано в последнем бэкапе. И сравнивать текущее содержимое с ним, записывать в новый архив только то, что добавилось/изменилось. Собственно, tar почти так и делает, но с ним не всё так хорошо, см. ОП.

TheAnonymous ★★★★★
() автор топика

Тебе часом не backintime нужен? В смысле на момент бэкапа там сохраняется именно срез. Или тебе нужен именно diff? Тогда по-моему бакапилка по умолчанию Ubuntu 16.04 именно так и работает.

Evgueni ★★★★★
()
Последнее исправление: Evgueni (всего исправлений: 1)
Ответ на: комментарий от TheAnonymous

И вообще, поясни подробнее.

Пишешь diff в git, он его сжимает. Файлы все .git удаляешь, если надо. Можно просто сделать git репу где надо и тащить её/diffы с неё, а не все файлы.

чем это будет лучше какого-нибудь rdiff-backup?

Не знаю. rdiff-backup, вроде, ничего не архивирует, а так - тоже самое, за исключением всяких фич фс, скорее всего. По мне - такой подход проще, если надо просто бекапить какие-нибудь сорцы.

crutch_master ★★★★★
()
Последнее исправление: crutch_master (всего исправлений: 1)
Ответ на: комментарий от TheAnonymous

записывать в новый архив только то, что добавилось/изменилось

А при восстановлении понадобятся все 100500 предыдущих бекапов? Да ну нафиг такое щщастье.

ashot ★★★★
()

https://github.com/opennet/FSBackup
Вот это очень похоже на то, что нужно. И там можно сохранять даже не просто атрибуты, а md5-хэши файлов последнего бэкапа (но считать долго).
Но какая-то сложная система, ещё и на мерзком перле, потому я делаю свой велосипед

#!/bin/bash
set -e

if [[ "$#" -ne 3 ]]; then
  echo "Usage: $0 snapshot_statefile backup.tar.gz source_directory/" >&2
  exit 1
fi

STATEFILE=`realpath "$1"`
ARCHIVE=`realpath "$2"`

pushd "$3" > /dev/null
trap 'popd > /dev/null' EXIT
NEWSTATE=`mktemp`
[[ -f "$STATEFILE" ]] && OLDSTATE="$STATEFILE" || OLDSTATE="/dev/null"
export LC_ALL=C
find . -printf "%p\0%y%l\0%s\0%T@\n" | sort > "$NEWSTATE"
set -o pipefail
comm -13 "$OLDSTATE" "$NEWSTATE" | awk -F'\0' '{print $1}' | tar --no-recursion -cvzpf "$ARCHIVE" -T -
cat "$NEWSTATE" > "$STATEFILE"
rm "$NEWSTATE"

Работает так же, как tar --listed-incremental, т.е. бэкапы можно создавать автономно, главное, чтобы был в наличии этот statefile.

Знатоки bash скриптов, посоветуете что исправить?
Ну или, может таки есть ещё какие-то более человеческие решения (для той же цели)?

TheAnonymous ★★★★★
() автор топика
Ответ на: комментарий от crutch_master

Да причем здесь git? Главное, опять суть, бэкап должен создаваться без доступа к предыдущим бэкапам. А git хранит все данные (=бэкапы), пусть и в сжатом виде. Это всякие сорцы хорошо жмутся, а ты попробуй фотографии в гите хранить, или музыку

TheAnonymous ★★★★★
() автор топика
Ответ на: комментарий от TheAnonymous

Главное, опять суть, бэкап должен создаваться без доступа к предыдущим бэкапам.

Так тебе вообще надо какой-то велосипед типа бд с хешами, атрибутами файлов и логом действий. Системы бекапов всё-таки подразумевают то, что у тебя всё это под рукой в полном объёме, а не список чего есть и где.

Это всякие сорцы хорошо жмутся

Да какая разница? Они что в git'е жмуться каким-нибудь gzip'ом, что в tar ты их тащишь. В gite плюс в том, что всё логируется на предмет того, что пришло, что ушло, что поменялось. Ну, если у тебя фотки, то да, смысла особо нет, хотя.

crutch_master ★★★★★
()
Последнее исправление: crutch_master (всего исправлений: 1)
Ответ на: комментарий от TheAnonymous

Знатоки bash скриптов, посоветуете что исправить?

На вскидку, если это у вас весь отдельный файл скрипта, то «trap 'popd > /dev/null' EXIT» — это нонсенс, впрочем как и popd. \0 - защищает от имени файла с \n, но у вас всё равно сломается алгоритм при этом, а работа sort и comm с нулевыми символами всегда и везде — под большим вопросом.

Так что я бы сильно переписал, получая от find только список файлов, после чего фильтровал бы вывод насчёт '.' и получал данные о файле через stat, а само имя квотировал через printf '%q'

vodz ★★★★★
()
Последнее исправление: vodz (всего исправлений: 2)
Ответ на: комментарий от TheAnonymous

Вот что получилось:

#!/bin/bash

if [[ "$#" -ne 3 ]]; then
  echo "Usage: $0 snapshot_statefile backup.tar.gz source_directory" >&2
  exit 1
fi

declare -a EXCLUDES

STATEFILE=`realpath "$1"`
if [[ -f "$STATEFILE" ]]; then
	OLDSTATE="$STATEFILE"
	EXCLUDES+=($(stat -c '%t.%T/%i' "$OLDSTATE"))
else
	OLDSTATE="/dev/null"
fi

ARCHIVE=`realpath "$2"`

cd "$3" || exit 1

SORT_IN=`mktemp`
SORT_OUT="$SORT_IN.out"

make_state_line() {
	local attr_all=$(stat -c '%t.%T/%i %s|%Y|%F' "$1") attr type i

	attr=${attr_all%% *}
	for i in "${EXCLUDES[@]}"; do
		[[ $attr == $i ]] && return 1
	done
	attr_all=${attr_all#$attr }
	attr=${attr_all%|*}
	type=${attr_all#"$attr|"}
	if [[ $type == "symbolic link" ]]; then
		type=l
	else
		type=${type:0:1}
	fi
	printf "%q %s\n" "$1" "$attr|$type"
}

scan_a() {
	local p found

	[[ $2 -eq 1 ]] && EXCLUDES+=($(stat -c '%t.%T/%i' "$SORT_IN"))

	for p in "$1/"* "$1/."* ; do
		if [[ -d "$p" && ! -L "$p" ]]; then
			[[ "${p:0-2:2}" == /. || "${p:0-3:3}" == /.. ]] && continue
			scan_a "$p" 0 && found=1
		elif [[ -e "$p" ]]; then
			make_state_line "$p" && found=1
		fi
	done
	if [[ -z $found ]]; then
		make_state_line "$1"
		return 1
	fi
	return 0
}

scan_a "." 1 > "$SORT_IN"

sort "$SORT_IN" > "$SORT_OUT"

while IFS= read -r line; do
	eval "echo ${line% *}"
done > "$SORT_IN" < <(comm -13 "$SORT_OUT" "$OLDSTATE")

mv "$SORT_OUT" "$STATEFILE"
[[ -s $SORT_IN ]] && tar -cvzpf "$ARCHIVE" -T "$SORT_IN"
rm -f "$SORT_IN"
Изменения: файлы могут содержать \n в именах, не архивирует сами файлы со статусами и временные, просто добавлять ещё файлы для исключения, можно звать с каталогом '.', не вызывается tar если архивировать нечего, вызывает tar c обычным поведением — с рекурсией, что позволяет записать каталог с файлами, если они там появились файлы после создания статусного файла. Не вызывает awk. Файл статуса не содержит \0. Но медленнее, так как на каждый файл зовёт stat и вообще сама логика на bash. Но так всегда, когда надо что-то логическое сделать, в данном случае производить игнорирование файлов по списку.

vodz ★★★★★
()
Ответ на: комментарий от vodz

«trap 'popd > /dev/null' EXIT» — это нонсенс, впрочем как и popd.

Да, вообще прикол. Действительно, и вместо pushd пойдёт cd.
При завершении скрипта же возвращается старая рабочая директория, для mc даже костыль есть, чтобы при выходе сохранялся путь, открытый в mc

защищает от имени файла с \n, но у вас всё равно сломается алгоритм

Вообще, впервые узнаю, что имя файла может содержать '\n'. Я считаю, держать такие файлы - ССЗБ, но проверку сделать можно, чтобы вываливалось с ошибкой раньше. Но в моём случае find пишет на месте «\n» знак вопроса.

работа sort и comm с нулевыми символами всегда и везде — под большим вопросом

LC_ALL=C разве это не решает?

получал данные о файле через stat, а само имя квотировал через printf '%q'

Запускать по процессу на каждый файл - как-то не очень, особенно, когда их в сумме много тысяч.

#!/bin/bash
set -e

if [[ "$#" -ne 3 ]]; then
  echo "Usage: $0 snapshot_statefile backup.tar.gz source_directory/" >&2
  exit 1
fi

STATEFILE=`realpath "$1"`
ARCHIVE=`realpath "$2"`

cd "$3"
NEWSTATE=`mktemp`
[[ -f "$STATEFILE" ]] && OLDSTATE="$STATEFILE" || OLDSTATE="/dev/null"
export LC_ALL=C
find . -printf "%p\0%y%l\0%m\0%U\0%G\0%s\0%T@\n" | sort > "$NEWSTATE"
awk -F'\0' 'NF != 7 {print "error:"; print; err = 1} END {exit err}' "$NEWSTATE"
set -o pipefail
comm -13 "$OLDSTATE" "$NEWSTATE" | awk -F'\0' '{print $1}' | tar --no-recursion -cvzpf "$ARCHIVE" -T -
cat "$NEWSTATE" > "$STATEFILE"
rm "$NEWSTATE"

Ещё с атрибутами теперь пишутся права (chmod), UID и GID

TheAnonymous ★★★★★
() автор топика
Ответ на: комментарий от Harliff

Крутая вещь, тоже то, что нужно. Можно отделять от архива «каталог», и использовать его как основу для инкрементального бэкапа.
Смущает только, что формат свой такой нестандартный, что даже всеядный 7z его не понимает

TheAnonymous ★★★★★
() автор топика

Пробовал dar. Но так и не осилил diff. Новый срез был по размеру почти такой же, как и оригинал. Хотя особо ничего не менялось.

Пример:

# full
sudo dar -c /media/data/linux -R / -D -z -P media -P dev -P proc -P run -P sys -P tmp
# diff
sudo dar -c /media/data/linux_diff -R / -D -z -P media -P dev -P proc -P run -P sys -P tmp -A /media/data/linux

RazrFalcon ★★★★★
()
Последнее исправление: RazrFalcon (всего исправлений: 1)
Ответ на: комментарий от TheAnonymous

Вообще, впервые узнаю, что имя файла может содержать '\n'.

Ну так запрещен только '\0' и с оговорками '/'. Но, впрочем, тогда надо для tar формировать список «null-terminated names» и вызывать с флагом --null.

LC_ALL=C разве это не решает?

Нет, это другое, у вас только для сортировки даст эффект: будет сортировать по значению побайтно. Имелось в виду, что с нулями раньше обычное дело, когда всё либо игнорировалось, либо глючило. Я вот не уверен насчёт утилит sort/comm/awk из busybox.

Запускать по процессу на каждый файл - как-то не очень, особенно, когда их в сумме много тысяч.

Я у себя проверил, разница впечатляющая, где-то в 60 раз на моём полном списке файлов от корня. Но, в абсолютном выражении это всё равно 10 секунд против 0.1 секунды. Ну 10 секунд не бог весь какое время. Но, собственно, я же обосновал, почему к этому всегда приходят. Как только скрипт вылезает из режима простейший однострочник, то сразу появляется необходимость всё делать самому руками, так как все хотелки внешним программам невозможно никакими ключами объяснить. У find можно отключить сканирование других файловых систем, отличных от текущей, но вот исключить не все, а по списку — уже нет такой опции.

Ещё с атрибутами теперь пишутся права (chmod), UID и GID

Да, я тоже об этом подумал.

А вообще, если время будет, я перепишу на ассоциативных массивах, выкину sort | comm и временные файлы, добавлю по умолчанию запрет сканирования /proc /tmp и т д, возможно добавлю в статусный файл имя архива, где лежит последняя версия конкретного файла...

vodz ★★★★★
()
Последнее исправление: vodz (всего исправлений: 1)
Ответ на: комментарий от vodz

Как только скрипт вылезает из режима простейший однострочник

надо брать нормальный язык программирования и писать, а не пердолиться в шелл

anonymous
()
Ответ на: комментарий от anonymous

надо брать нормальный язык программирования и писать, а не пердолиться в шелл

Ну так то да. Но вот я только алгоритм «бизнес логики» уже три раза переписал, а это лучше делать на ЯП высокого уровня, на низком мыслительный процесс пойдёт на реализацию. Заодно нашёл для себя новое в bash. Оказывается, для ассоциативных массивов unset с ключем в переменной надо делать только так:

unset ASSOC_DIM['$key']
При использовании же двойных кавычек оно вроде бы работает, пока в key не будет обратного слеша...

vodz ★★★★★
()
Ответ на: комментарий от TheAnonymous

Запускать по процессу на каждый файл - как-то не очень, особенно, когда их в сумме много тысяч.

Таки переписал, stat теперь вызывает пачками для всех файлов/каталогов в каталоге, но не более 250 за раз. В statfile сохраняет номер бекапа, добавил ключи вызова, стало удобнее запускать. Других программ не вызывает (кроме stat и tar) и временных файлов не создаёт. С ключём -v даёт подробную статистику. Это полезно хотя бы потому, что каждая тыща файлов в ассоциативный массив влезает всё медленне и медленнее :(

#!/bin/bash

declare -a EXCLUDE=("/proc" "/tmp" "/var/tmp" "/dev" "/sys" "/run")
declare -a OUT DIRS=(/)

STATEFILE=statefile.lst
ARCHIVE=$(date '+%Y%m%d_%H%M%S')
ARCHIVE_SUFFIX=".tar.gz"

usage() {
  echo "Usage: $0 [-s STATEFILE] [-e exclude] [-v] [DIRECTORIES...]"
  echo
  echo "    make a sequency archive with state file, compared changes:"
  echo "        modify time, size, mode, uid and gid"
  echo
  echo "    ARCHIVE is NUMBER_DATE_TIME$ARCHIVE_SUFFIX,"
  echo "        NUMBER is of max BACKUP_NUMBER+1 from statefile"
  echo "        DATE_TIME format see from current: $ARCHIVE"
  echo "    STATEFILE - snapshot state of previos backups, default '$STATEFILE'"
  echo "        format: filename BACKUP_NUMBER=MODE:UID.GID_SIZE|TIMESTAMP|type"
  echo "                type: [dryoehfbc] - directory, regular, symlink, socket,"
  echo "                      semaphore, shared, fifo, block, character"
  echo -n " excludes default: ARCHIVE STATEFILE DIRECTORIES"
  for a in "${EXCLUDE[@]}"; do
	echo -n " $a"
  done
  echo
  echo "    default DIRECTORIES is ${DIRS[@]}"
  echo "    -v - verbose"
  exit 2
} >&2

verbose=0
while getopts ":e:s:v" o; do
    case "$o" in
    e) EXCLUDE+=("$OPTARG") ;;
    s) STATEFILE="$OPTARG" ;;
    v) verbose=1 ;;
    *) usage;;
    esac
done
shift $((OPTIND-1))

if [[ $# -ne 0 ]]; then
	for i in ${!DIRS[@]}; do
		unset DIRS[$i]
	done
	for d in "$@"; do
		if [[ ! -d "$d" ]]; then
			echo "$0: '$d' is not directory" >&2
			usage
		fi
		DIRS+=("$d")
	done
fi

declare -A A_OLD

max_b=0
a_old_n=0
if [[ -f "$STATEFILE" ]]; then
	while IFS= read -r line; do
		f=${line% *}
		attr=${line#"$f "}
		if [[ $f == $line || $attr == $line || -n ${attr#?*=?*:?*.?*_?*|?*|?} ]]; then
			echo "$0: '$STATEFILE' have strange format, exiting" >&2
			exit 1
		fi
		A_OLD[$f]=$attr
		attr=${attr%=*}
		[[ $max_b -lt $attr ]] && max_b=$attr
		(((++a_old_n%1000)==0 && verbose)) && echo -n "$0: $STATEFILE $a_old_n lines loaded"$'\r'
	done < "$STATEFILE"
	((++max_b))
fi
[[ $verbose -ne 0 ]] && echo "$0: $STATEFILE $a_old_n lines loaded"
exec 9> "$STATEFILE"

i=0
while IFS= read -r line; do
	EXCLUDE[i++]=$line
done < <(stat '-c' '%d/%i' "${EXCLUDE[@]}" "$STATEFILE" "${DIRS[@]}")

all=0
new_files=0
changed_files=0
make_state_line() {
	local attr_all=$2 attr type f

	attr=${attr_all%|*}
	type=${attr_all#"$attr|"}
	if [[ ${type:0:1} == s ]]; then
		type=${type:1:1}
	else
		type=${type:0:1}
	fi
	printf -v f '%q' "$1"
	attr_all="$attr|$type"
	((all++))
	if [[ -n ${A_OLD[$f]} ]]; then
		((a_old_n--))
		if [[ $attr_all == ${A_OLD[$f]#*=} ]]; then
			echo "$f ${A_OLD[$f]}" >&9
			unset A_OLD['$f']
			return 0
		else
			unset A_OLD['$f']
			((changed_files++))
		fi
	else
		((new_files++))
	fi
	echo "$f $max_b=$attr_all" >&9
	OUT+=("$1")
}

scan_a() {
	local p a1=$1
	local -a FILES=("$1")

	tst_exclude_fill_state() {
		local attr_all attr i=0

		while read attr_all; do
			attr=${attr_all%=*}
			for a in "${EXCLUDE[@]}"; do
				if [[ $attr == $a ]]; then
					attr=
					break
				fi
			done
			if [[ -n $attr ]]; then
				if [[ ${attr_all} == ${attr_all%|directory} || ${FILES[i]} == $a1 ]]; then
					make_state_line "${FILES[i]}" "${attr_all#$attr=}"
				else
					DIRS+=("${FILES[i]}")
				fi

			fi
			((i++))
		done < <(stat '-c' '%d/%i=%a:%u.%g_%s|%Y|%F' "${FILES[@]}")
	}

	for p in "$1/"* "$1/."* ; do
		[[ -e "$p" ]] || continue
		[[ "${p:0-2:2}" == /. || "${p:0-3:3}" == /.. ]] && continue
		FILES+=("$p")
		if [[ ${#FILES[*]} -eq 250 ]]; then
			tst_exclude_fill_state
			FILES=()
			a1=
		fi
	done
	if [[ ${#FILES[*]} -ne 0 ]]; then
		tst_exclude_fill_state
	fi
}

while [[ ${#DIRS[*]} -ne 0 ]]; do
	for i in ${!DIRS[@]}; do
		[[ $verbose -ne 0 ]] && echo -n "${DIRS[i]} "
		scan_a "${DIRS[i]}"
		unset DIRS[$i]
		[[ $verbose -ne 0 ]] && echo "(S:$all N:$new_files C:$changed_files)"
	done
done

[[ $verbose -ne 0 ]] && echo Scaned=$all New_files=$new_files Changed_files=$changed_files Deleted_files=$a_old_n

for f in "${!A_OLD[@]}"; do
	echo "$f ${A_OLD[$f]}" >&9
	[[ $verbose -ne 0 ]] && echo "-$f"
done

if [[ -n ${OUT[0]} ]]; then
	for fq in "${OUT[@]}"; do
		printf '%s\0' "$fq"
	done | tar -cvzpf ""${max_b}_$ARCHIVE$ARCHIVE_SUFFIX"" --no-recursion --null -T -
fi

vodz ★★★★★
()
Последнее исправление: vodz (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.