Мой скрипт, с помощью которого я делаю бэкапы в Linux
Обожаю UNIX-way, тут бэкапы можно делать значительно более гибкими.
Для бэкапа home директории я использую обычный tar с инкрементацией и шифрую его своим gpg ключом.
Для других файлов, например, для бэкапов моих видео, которые я записываю для ютуба я использую rsync. RSYNC более рационально использовать, когда не критична синхронизация большого количества файлов
#!/bin/bash
NOW=$(date +%Y%m%d%H%M)
MNOW=$(date +%Y%m)
BACKUP_HOME="/tmp/home/"
EMAIL="devpew"
ARCHIVES_DIR="/tmp/backup/"
DOW=`date +%a` # Day of the week e.g. Mon
DOM=`date +%d` # Date of the Month e.g. 27
DM=`date +%d%b` # Date and Month e.g. 27Sep
if [[ ! -d ${ARCHIVES_DIR}${MNOW} ]]
then
mkdir ${ARCHIVES_DIR}${MNOW}
else
echo &>/dev/null
fi
tar --exclude-from=/home/dm/mybin/.backup.excludes -v -z --create --file ${ARCHIVES_DIR}${MNOW}/${NOW}.tar.gz --listed-incremental=${ARCHIVES_DIR}${MNOW}/${MNOW}.snar $BACKUP_HOME &> ${ARCHIVES_DIR}${MNOW}/${NOW}.log
if [ $(ls -d ${ARCHIVES_DIR}${MNOW}/*.tar.gz 2> /dev/null | wc -l) != "0" ]
then
gpg -r $EMAIL --encrypt-files ${ARCHIVES_DIR}${MNOW}/*.tar.gz \
&& rm -rf ${ARCHIVES_DIR}${MNOW}/*.tar.gz
fi
scp ${ARCHIVES_DIR}${MNOW}/${NOW}.tar.gz.gpg ${ARCHIVES_DIR}${MNOW}/${MNOW}.snar dm@192.168.0.152:/home/dm/backup/${MNOW}
Если нужен более гибкий инкремент второго уровня, например, по неделям, то можно использовать такие условия
DOW=`date +%a` # Day of the week e.g. Mon
DOM=`date +%d` # Date of the Month e.g. 27
DM=`date +%d%b` # Date and Month e.g. 27Sep
if [ $DOM = "01" ]; then
echo 'this is monthly backup'
fi
if [ $DOW = "Sun" ]; then
echo 'this is weekly backup'
else
echo 'this is daily backup'
fi
How it works
Теперь, коротко о том, что делает этот скрипт
Скрипт перейдет в указанную директорию для бекапов и создаст в ней директорию с именем года и месяца в формате “202205” если сейчас май 2022 года.
Далее все бэкапы за май будут находиться в этой папке.
Далее если в папке нет файла с инкрементом (например, мы впервые запустили скрипт или начался новый месяц) то у нас создастся полный бекап всей системы.
В дальнейшем при запуске этого скрипта будут создаваться инкременты от текущего полного бекапа
Кроме того появится файл с логом
После того как у нас сделался бекап, он у нас зашифруется нашим GPG ключом а файл TAR удалится.
После этого мы скопируем наш бэкап к нам на сервер
Exclude
Если нужно задать исключения. То есть файлы или директории, которые не нужно бекапить, то сделать это можно в файле с исключениями. Тут нужно быть аккуратным, любой лишний пробел может все сломать
➜ mybin cat /home/dm/mybin/.backup.excludes
/tmp/home/Nextcloud
/tmp/home/.cache
/tmp/home/youtube-videos
Кроме того, ничего не будет работать, если вы поставите слэш в конце.
Например строка /tmp/home/Nextcloud
будет работать, а вот строка /tmp/home/Nextcloud/
работать уже не будет. Так что будьте аккуратны
Если нужно распаковать
У нас делаются инкрементальные бекапы и шифруются. Если нам нужно получить данные, то для начала нам нужно расшифровать файл
Расшифровать можно командой
gpg --output 202205122134.tar.gz --decrypt 202205122134.tar.gz.gpg
После этого, начнем распаковывать tar начиная с самого первого. Для начала распаковываем архив от первого числа.
tar --extract --verbose --listed-incremental=/dev/null --file=202205010101.tar.gz
И после этого распаковываем остальные инкременты, если нужно восстановить состояние системы, например, на 11 число, то нужно последовательно распаковать tar-архивы со 2 по 11 в ту же папку
Если это кажется слишком долгим процессом и вы часто восстанавливаете данные, то можно добавить инкремент второго уровня, например, недельный.
Или если вручную для вас это кажется слишком длительным процессом, можете накидать небольшой скрипт для распаковки. В простейшем случае так:
tar --extract --incremental --file file0.tar
tar --extract --incremental --file file1.tar
tar --extract --incremental --file file2.tar
Или, например, так:
for i in *.tbz2; do tar -xjGf "$i"; done;
Если нужно извлечь только конкретные каталоги из архива:
tar -xjGf levelX.tar --wildcards 'example/foo*' 'example/bar*'
Autorun
Если бы в убунте или дебиане, то вам нужно запускать этот скрпит через крон. В арче нет крона и автозапуск делается иначе. В этом примере будем запускать наш скрипт каждый день в 03:30
Нужно создать файл
sudo nvim /usr/lib/systemd/system/backup.service
[Unit]
Description=backup
[Service]
Type=simple
ExecStart=/home/dm/mybin/backup
И файл
sudo nvim /usr/lib/systemd/system/backup.timer
[Unit]
Description=my backup
[Timer]
OnCalendar=*-*-* 03:30:00
[Install]
WantedBy=multi-user.target
После этого можем запустить наш сервис
sudo systemctl start backup.timer
sudo systemctl enable backup.timer
После этого проверим добавился ли он командой
sudo systemctl list-timers --all
или командой
sudo systemctl status backup.timer
Remove old backups
Кроме того, что мы постоянно создаем бэкапы, нам нужно удалять старые для того чтобы не забить все место на диске. Если у вас есть скрипт для этого поделитесь, а я опубликую сюда ваше решение.
Комментарии (26)
garwall
18.05.2022 08:52+4трамвай из буханки.жпг
Для таких дел есть уже более толковые тулзы с шифрованием, сжатием, дедупликацией и прочими плюшками и финтифлюшками. - restic, borg
ALexhha
18.05.2022 10:48+2Для таких дел есть уже более толковые тулзы с шифрованием, сжатием,
дедупликацией и прочими плюшками и финтифлюшками. - restic, borgэто же классика - каждый админ должен написать свою систему бекапов )))
saipr
18.05.2022 08:58Кроме того, ничего не будет работать, если вы поставите слэш в конце.
Я бы сказал просто, что пути к файлам и каталогам должны быть без завершающего слэша (без "/").
Я их никогда там не ставлю и поэтому у меня такого вопроса не возникает.
Skyline_XXX
18.05.2022 09:34./backup.sh: строка 7: +%a: команда не найдена
./backup.sh: строка 8: +%d: команда не найдена
./backup.sh: строка 9: +%d%b: команда не найдена
./backup.sh: строка 10: синтаксическая ошибка в условном выражении
./backup.sh: строка 12: синтаксическая ошибка рядом с «"»
./backup.sh: строка 12: `mkdir " class="formula inline">{MNOW}'devpew Автор
18.05.2022 11:20Добрый день! В статье с форматированием произошла катастрофа
Везде ARCHIVE_DIR заменилось на " class="formula inline">
Очень странный скрипт получился. Сейчас перепроверьте пожалуйстаSkyline_XXX
19.05.2022 19:49Спасибо!
Так намного лучше!
Пока так:
gpg: devpew: пропущено: Нет открытого ключа
gpg: encryption of '/tmp/backup/202205/202205191946.tar.gz' failed: Нет открытого ключа
С ключами разобраться моя проблема.
ALexhha
18.05.2022 12:43+1Мой скрипт, с помощью которого я делаю бэкапы в Linux
backup скрипт на bash без -eux - да вы просто смельчак
rm -rf ${ARCHIVES_DIR}${MNOW}/*.tar.gz
и в один прекрасный момент у вас переменные ARCHIVES_DIR/MNOW окажутся неопределенными и команда превратится в
rm -rf /*.tar.gz
со всеми вытекающими
ghostinushanka
18.05.2022 12:47+1if [[ ! -d ${ARCHIVES_DIR}${MNOW} ]] then mkdir ${ARCHIVES_DIR}${MNOW} else echo &>/dev/null fi
Зачем вот эта вся конструкция?
Как я её читаю — если нет директории с определённым названием, создай директорию, в противном случае пошли пустое эхо в нуль. Вы это зачем делаете? Чтобы получить нулевой код на случай если директория уже есть? Так у вас в скрипте нету проверки ненулевых значений `set -e` и скрипт всёравно не остановится.
Хотели избежать выписывания «File exists» когда директория уже присутствует? Можно перенавравить это самое сообщение в нуль.mkdir ${ARCHIVES_DIR}${MNOW} 2>/dev/null
Но тогда у вас ненулевой код будет после mkdir, так? Но как я написал выше — вы и так с этим ничего не делаете.
Лучше всего будет просто сделать вот так:mkdir -p ${ARCHIVES_DIR}${MNOW}
И всё. `man mkdir` обьяснит почему.
Дальшеif [ $(ls -d ${ARCHIVES_DIR}${MNOW}/*.tar.gz 2> /dev/null | wc -l) != "0" ] then gpg -r $EMAIL --encrypt-files ${ARCHIVES_DIR}${MNOW}/*.tar.gz \ && rm -rf ${ARCHIVES_DIR}${MNOW}/*.tar.gz fi
Вы выполняете листинг файлов, перенаправляете сообщение об ошибке что таких файлов нет в нуль, а потом считаете строки. Но ведь сообщение об ошибке, что таких файлов нет само по себе означает что их нет и считать ничего не надо, разьве не так?
Переделываем:ls -d ${ARCHIVES_DIR}${MNOW}/*.tar.gz 2> /dev/null \ && gpg -r $EMAIL --encrypt-files ${ARCHIVES_DIR}${MNOW}/*.tar.gz \ && rm -rf ${ARCHIVES_DIR}${MNOW}/*.tar.gz
Ну а прочтение мануала gpg для того чтобы узнать как себя он поведёт если получит пустой список на входе `--encrypt-files` (и соответственно требуется-ли вообще проверять существуют-ли файлы) я оставлю в качестве разминки для мозговdevpew Автор
18.05.2022 14:55Денис, спасибо большое. Тот случай когда решил поделиться чем-то на хабре, но сам получил пользу
devpew Автор
19.05.2022 00:19Единственное, что не понял про вот эту часть "Ну а прочтение мануала gpg для того чтобы узнать как себя он поведёт если получит пустой список на входе
--encrypt-files
(и соответственно требуется-ли вообще проверять существуют-ли файлы) я оставлю в качестве разминки для мозгов"В моем варианте мы ведь как раз проверяем, что файл должен быть
if [ $(ls -d ${ARCHIVES_DIR}${MNOW}/*.tar.gz 2> /dev/null | wc -l) != "0" ]
ghostinushanka
19.05.2022 13:41Я имел ввиду, что проверять этого вообще не надо.
Если флаг `--encrypt-files` получит на входе пустой список — gpg не сработает.
Будет выписано сообщение об ошибке и завершена работа с ненулевым кодом. Так как у вас `rm` вызывается по цепочке через `&&`, срабатывать оно будет только если `gpg` вернёт нулевой код (отработает как надо)
Поэтому вполне достаточно:gpg -r $EMAIL --encrypt-files ${ARCHIVES_DIR}${MNOW}/*.tar.gz \ && rm -rf ${ARCHIVES_DIR}${MNOW}/*.tar.gz
Можно ещё при желании «зактнуть» gpg отправкой сообщения об ошибке в нуль или файл по желанию.
Ну и как указали в другой ветке, файлы стирать с флагом `-r` лучше не надо, отсюдаgpg -r $EMAIL --encrypt-files ${ARCHIVES_DIR}${MNOW}/*.tar.gz 2>/dev/null \ && rm -f ${ARCHIVES_DIR}${MNOW}/*.tar.gz
Kirikekeks
18.05.2022 12:52+1Я давно *.gz забыл в пользу *.xz. И польза весьма большая, как по времени создания, а уже по времени извлечения - огромная. И по размеру полученному в результате.
vviz
18.05.2022 21:16Для продакшена без UI с возможность выбора на уровне файла - не гуд.
Для дома - инкрементный бэкап при современных объемах носителей - мазохизм.
tar(если нужно сохранить chown/chmod), p7zip(сложный пароль), davfs в облаке.
ИМХО.
Mas73r
19.05.2022 00:37Есть backup-manager (https://github.com/sukria/Backup-Manager) -- в Debian уже в пакетах идет. Все основные действия из статьи умеет делать сам.
nochkin
Если надо просто удалить файлы, например, старее одной недели по маске, то можно с помощью find сделать что-то типа такого:
find /tmp/backup/ -name "files*.tar" -type f -mtime +7d -exec rm -f {} \;
Надо только настроить под себя, что бы полный бекап не прибить случайно.
AlexHighTower
а есть вариант «в одну строчку» удалить все старее чем Х единиц времени, но оставить один файл?
т.е. вот создаю я бэкапы и удаляю старые
потом случилось что то и бэкапы перестали по какой то причине создаваться, а удаление старых продолжается, и чтобы не потерять самый последний из имеющихся бэкапов его бы здорово оставлять в таких случаях…
nochkin
Если оставаться на "find", то можно сделать "touch" на те файлы, которые не надо удалять перед самой командой удаления или что-то типа такого.
Или переименовывать что бы можно было по маске отфильтровать только то, что нужно.
AlexHighTower
т.е. задача сводится к тому, что перед удалением надо посмотреть какие файлы попадают под удаление, проанализировать что можно удалять и потом только удалить…
я прошиваю потому что как и многие писал свои скрипты бэкапа и вот именно такую задачу и решал, получилась хоть и небольшая но простынька кода на баше чтобы эту ситуацию проверять, вот и интересно есть ли иные варианты…
nochkin
Можно и так, конечно. Но я имел ввиду немного другое: в процессе работы скрипта должно быть достаточно проверок, что бы понять что backup слопал северный пушной зверёк и вовремя остановиться.
То есть, при проблеме сохранения нет смысла делать чистку вообще.
P.S.: Я такие задачи обычно решаю не шел-скриптами, а python'ом. Там проще в плане перехвата ошибок и анализа. Но это уже отступление от задачи, конечно.
ghostinushanka
А что в вашем понимании «в одну строчку»?
Цикл и в одну строчку записать можно.
С другой стороны, если вы себе сами придумали формат названия бэкапов, что вам мешает тем же find получить список всех, и дальше через пайпу первый из списка удалить (например с помощью sed), остальные стереть. Если будет пусто — понятное дело ничего не сотрётся а первый сохранён будет.
Вариантов как это сделать — масса.
devpew Автор
ага, только может произойти ситуация при которой скрипт бекапа по какой-то причине перестанет работать и вы этого не заметите, но вот скрипт с удалением работать не перестанет и удалит все)
nochkin
"Замечать" -- это задача бекап скрипта. Если там ошибка, то до "find" вообще не должно доходить.