Мой скрипт, с помощью которого я делаю бэкапы в 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)


  1. nochkin
    18.05.2022 05:39
    +1

    Если надо просто удалить файлы, например, старее одной недели по маске, то можно с помощью find сделать что-то типа такого:

    find /tmp/backup/ -name "files*.tar" -type f -mtime +7d -exec rm -f {} \;

    Надо только настроить под себя, что бы полный бекап не прибить случайно.


    1. AlexHighTower
      18.05.2022 10:29

      а есть вариант «в одну строчку» удалить все старее чем Х единиц времени, но оставить один файл?
      т.е. вот создаю я бэкапы и удаляю старые
      потом случилось что то и бэкапы перестали по какой то причине создаваться, а удаление старых продолжается, и чтобы не потерять самый последний из имеющихся бэкапов его бы здорово оставлять в таких случаях…


      1. nochkin
        18.05.2022 17:25

        Если оставаться на "find", то можно сделать "touch" на те файлы, которые не надо удалять перед самой командой удаления или что-то типа такого.

        Или переименовывать что бы можно было по маске отфильтровать только то, что нужно.


        1. AlexHighTower
          18.05.2022 19:57

          т.е. задача сводится к тому, что перед удалением надо посмотреть какие файлы попадают под удаление, проанализировать что можно удалять и потом только удалить…

          я прошиваю потому что как и многие писал свои скрипты бэкапа и вот именно такую задачу и решал, получилась хоть и небольшая но простынька кода на баше чтобы эту ситуацию проверять, вот и интересно есть ли иные варианты…


          1. nochkin
            18.05.2022 21:34

            Можно и так, конечно. Но я имел ввиду немного другое: в процессе работы скрипта должно быть достаточно проверок, что бы понять что backup слопал северный пушной зверёк и вовремя остановиться.

            То есть, при проблеме сохранения нет смысла делать чистку вообще.

            P.S.: Я такие задачи обычно решаю не шел-скриптами, а python'ом. Там проще в плане перехвата ошибок и анализа. Но это уже отступление от задачи, конечно.


      1. ghostinushanka
        18.05.2022 21:19

        А что в вашем понимании «в одну строчку»?
        Цикл и в одну строчку записать можно.

        С другой стороны, если вы себе сами придумали формат названия бэкапов, что вам мешает тем же find получить список всех, и дальше через пайпу первый из списка удалить (например с помощью sed), остальные стереть. Если будет пусто — понятное дело ничего не сотрётся а первый сохранён будет.

        Вариантов как это сделать — масса.


    1. devpew Автор
      18.05.2022 11:22

      ага, только может произойти ситуация при которой скрипт бекапа по какой-то причине перестанет работать и вы этого не заметите, но вот скрипт с удалением работать не перестанет и удалит все)


      1. nochkin
        18.05.2022 17:26

        "Замечать" -- это задача бекап скрипта. Если там ошибка, то до "find" вообще не должно доходить.


  1. garwall
    18.05.2022 08:52
    +4

    трамвай из буханки.жпг

    Для таких дел есть уже более толковые тулзы с шифрованием, сжатием, дедупликацией и прочими плюшками и финтифлюшками. - restic, borg


    1. ALexhha
      18.05.2022 10:48
      +2

      Для таких дел есть уже более толковые тулзы с шифрованием, сжатием,
      дедупликацией и прочими плюшками и финтифлюшками. - restic, borg

      это же классика - каждый админ должен написать свою систему бекапов )))


  1. saipr
    18.05.2022 08:58

    Кроме того, ничего не будет работать, если вы поставите слэш в конце.

    Я бы сказал просто, что пути к файлам и каталогам должны быть без завершающего слэша (без "/").
    Я их никогда там не ставлю и поэтому у меня такого вопроса не возникает.


  1. 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}'


    1. devpew Автор
      18.05.2022 11:20

      Добрый день! В статье с форматированием произошла катастрофа
      Везде ARCHIVE_DIR заменилось на " class="formula inline">
      Очень странный скрипт получился. Сейчас перепроверьте пожалуйста


      1. Skyline_XXX
        19.05.2022 19:49

        Спасибо!

        Так намного лучше!

        Пока так:
        gpg: devpew: пропущено: Нет открытого ключа
        gpg: encryption of '/tmp/backup/202205/202205191946.tar.gz' failed: Нет открытого ключа

        С ключами разобраться моя проблема.


  1. 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

    со всеми вытекающими


    1. devpew Автор
      18.05.2022 14:55

      Спасибо за подсказку. А set -eux надо в самом начале файла со скриптом ставить? или он в .zshrc устанавливается?


      1. ALexhha
        18.05.2022 15:09

        #!/bin/bash -eux

        минимальный джентльменский набор. Можно еще добавить set -o pipefail


  1. ghostinushanka
    18.05.2022 12:47
    +1

    devpew

    if [[ ! -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` (и соответственно требуется-ли вообще проверять существуют-ли файлы) я оставлю в качестве разминки для мозгов


    1. devpew Автор
      18.05.2022 14:55

      Денис, спасибо большое. Тот случай когда решил поделиться чем-то на хабре, но сам получил пользу


    1. devpew Автор
      19.05.2022 00:19

      Единственное, что не понял про вот эту часть "Ну а прочтение мануала gpg для того чтобы узнать как себя он поведёт если получит пустой список на входе --encrypt-files (и соответственно требуется-ли вообще проверять существуют-ли файлы) я оставлю в качестве разминки для мозгов"

      В моем варианте мы ведь как раз проверяем, что файл должен быть

      if [ $(ls -d ${ARCHIVES_DIR}${MNOW}/*.tar.gz 2> /dev/null | wc -l) != "0" ]


      1. 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


  1. Kirikekeks
    18.05.2022 12:52
    +1

    Я давно *.gz забыл в пользу *.xz. И польза весьма большая, как по времени создания, а уже по времени извлечения - огромная. И по размеру полученному в результате.


  1. vviz
    18.05.2022 21:16

    Для продакшена без UI с возможность выбора на уровне файла - не гуд.

    Для дома - инкрементный бэкап при современных объемах носителей - мазохизм.

    tar(если нужно сохранить chown/chmod), p7zip(сложный пароль), davfs в облаке.

    ИМХО.


  1. Mas73r
    19.05.2022 00:37

    Есть backup-manager (https://github.com/sukria/Backup-Manager) -- в Debian уже в пакетах идет. Все основные действия из статьи умеет делать сам.


  1. terantul
    19.05.2022 03:17

    *пошёл сносить bareos и заливать везде скрипт ТСа


    1. devpew Автор
      19.05.2022 11:22

      Эт правильно) сложно придумать что-то более сложное чем Bareos. Хотя для чего-то сложного возможно оно нужно