BASH: скрипт бекапа /home с уведомлениями

By | 10/12/2018
 

Задача – написать скрипт для домашней машинки, что бы создавать бекапы по расписанию используя rsync, с уведомлениями на почту и в трее.

Бекапы хранятся на отдельном жестком диске.

Описание настройки отправки почты пользователю есть в посте Arch Linux: ssmtp – отправка локальной почты.

Ниже – процесс написания такого скрипта с примерами функций. Сам скрипт доступен в Github тут>>>.

Создаём каталог для скрипта:

sudo mkdir /opt/home-backups-bash
sudo chown setevoy:setevoy /opt/home-backups-bash

И начинаем писать скрипт.

Нам нужны будут следующие функции:

  1. проверка того, что /backup смонирован
  2. функция для создания уведомлений, которые будут выводиться на рабочий стол и на почту
  3. сама функция бекапа

check-backups-fs()

Начнём с проверки наличия /backups:

#!/usr/bin/env bash

BKP_ROOT="/backups"

check-backups-fs() {
    
    bkp_root=$1
    findmnt $bkp_root
}

[[ $(check-backups-fs $BKP_ROOT) ]] && echo "OK: $BKP_ROOT found." || { echo "ERROR: $BKP_ROOT not found. Exit."; exit 1; }

Проверяем:

./home-backups.sh
OK: /backups found.

Меняем BKP_ROOT на несуществующую директорию, проверяем ещё раз:

./home-backups.sh
ERROR: /backups1 not found. Exit.

notify()

Добавляем функцию для отправки уведомлений на экран, пока в минимальном варианте, потом можно будет добавить иконки в зависимости от статуса:

#!/usr/bin/env bash
  
BKP_ROOT="/backups"
HOME_DIR="/home/setevoy/"

check-backups-fs() {

    bkp_root=$1

    findmnt $bkp_root
}

notify () {

    event=$1
    home_dir=$2
    at=$(date +"%d-%m-%Y %H:%M:%S")

    echo $now
    notify-send "Backup $event" "Backup $event for the $home_dir at $at" --icon=dialog-information
    echo "Backup $event for the $home_dir at $at" | mail -v -s "Backup $event" setevoy
}

[[ $(check-backups-fs $BKP_ROOT) ]] && echo "OK: $BKP_ROOT found." || { echo "ERROR: $BKP_ROOT not found. Exit."; exit 1; }

notify "started" $HOME_DIR

В переменной HOME_DIR указание каталога заканчиваем слешом, см. тут>>>.

Через notify-send – создаём уведомление в трее, через mail – уведомление в почту.

Проверяем:

ОК, теперь можно начинать писать непосредственно функцию бекапирования.

mkbackup()

Создаём каталог для бекапов по крону:

sudo mkdir /backups/Backups/Cron

Обновляем скрипт – добавляем перменную BKP_DIR, функцию выполнения rsync, пока в --dry-run и --verbose:

#!/usr/bin/env bash
  
BKP_ROOT="/backups"
BKP_DIR="$BKP_ROOT/Backups/Cron"

HOME_DIR="/home/setevoy/"

check-backups-fs() {

    bkp_root=$1

    findmnt $bkp_root
}

notify () {

    event=$1
    home_dir=$2
    at=$(date +"%d-%m-%Y %H:%M:%S")

    notify-send "Backup $event" "Backup $event for the $home_dir at $at" --icon=dialog-information
}



mkbackup() {

    bkp_dir=$1
    home_dir=$2

    sudo rsync --dry-run --archive --verbose $home_dir $bkp_dir
}

[[ $(check-backups-fs $BKP_ROOT) ]] && echo "OK: $BKP_ROOT found." || { echo "ERROR: $BKP_ROOTH not found. Exit."; exit 1; }

notify "started" $HOME_DIR

if mkbackup $BKP_DIR $HOME_DIR; then
    notify "finished" $HOME_DIR
else
    notify "FAILED" $HOME_DIR
fi

Проверяем:

ОК, теперь укажем несуществую директорию, для проверки – rsync --dry-run --archive --verbose $home_dir/blabla:

Вроде всё работает – пробуем добавить задачу в cron.

Находим D-Bus сессию пользователя:

echo $DBUS_SESSION_BUS_ADDRESS
unix:path=/run/user/1000/bus

Добавляем задание переменной в скрипт:

#!/usr/bin/env bash
  
BKP_ROOT="/backups"
BKP_DIR="$BKP_ROOT/Backups/Cron"

HOME_DIR="/home/setevoy/"


check-backups-fs() {

...

Создаём файл лога:

sudo touch /var/log/home-backups.log
sudo chown setevoy:setevoy /var/log/home-backups.log

Создаём cron-задачу:

MAILTO=1th@example.kiev.ua
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"

* * * * * /opt/home-backups-bash/home-backups.sh &> /var/log/home-backups.log

Сохраняем, проверяем уведомления в трее и почте:

Лог:

tail /var/log/home-backups.log -f
Work/RTFM/Github/tests/.git/refs/heads/
Work/RTFM/Github/tests/.git/refs/heads/master
Work/RTFM/Github/tests/.git/refs/remotes/
Work/RTFM/Github/tests/.git/refs/remotes/origin/
Work/RTFM/Github/tests/.git/refs/remotes/origin/HEAD
Work/RTFM/Github/tests/.git/refs/remotes/origin/master
Work/RTFM/Github/tests/.git/refs/tags/
sent 8,865,794 bytes  received 710,489 bytes  2,736,080.86 bytes/sec
total size is 260,297,908,025  speedup is 27,181.52 (DRY RUN)

Уведомления:

UPD

Подумал, подумал, и решил, что нет – надо скрипт ещё обновить.

Во-первых – добавить getops(), и возможность вызова скрипта с разными опциями/параметрами, во-вторых – добавить исключения для rsync – зачем, например, бекапить /home/username/.cache?

Или можно сделать даже иначе – использовать не исключения, а наоборот – указывать только то, какие каталоги из /home/username добавлять в бекап.

Но, наверно, лучше задавать явный exclude – тогда, если в /home добавится новый каталог и его забудешь добавить в include – он не потеряется при бекапе.

Да и список исключений будет меньше, чем список каталогов, которые надо бекапить.

rsync() include и exclude

Сначала потестим.

Создаём каталог:

mkdir -p /tmp/include_this/dir1/dir2

Добавляем файлы:

touch include_this/file && touch include_this/dir1/{.hidden,file1} && touch include_this/dir1/dir2/file2

Выполняем rsync с опциями include && exclude:

rsync -r -v --include=include_this/*** --exclude=* /tmp/ /backups/test/
sending incremental file list
include_this/
include_this/file
include_this/dir1/
include_this/dir1/.hidden
include_this/dir1/file1
include_this/dir1/dir2/
include_this/dir1/dir2/file2
sent 382 bytes  received 104 bytes  972.00 bytes/sec
total size is 0  speedup is 0.00

Проверяем:

tree /backups/test/
/backups/test/
└── include_this
├── dir1
│   ├── dir2
│   │   └── file2
│   └── file1
└── file
3 directories, 3 files

ОК, это работает.

Теперь проверим exclude-from – что бы все файлы-каталоги для исключения держать в одном txt-файле.

Добавляем каталоги и файлы:

mkdir -p exclude_this/dir1/dir2
touch exclude_this/file && touch exclude_this/dir1/file1 && touch exclude_this/dir1/dir2/file2

Создаём файл exclude.txt, в котором задаём каталог exclude_this:

cat exclude.txt
exclude_this/***

Проверяем rsync:

rsync -r -v --exclude-from exclude.txt /tmp/ /backups/test/
sending incremental file list
.X0-lock
.org.chromium.Chromium.ZKtvRZ
dropbox-antifreeze-AeMiDY
dropbox-antifreeze-CMWTrD
dropbox-antifreeze-bjUr5j
dropbox-antifreeze-fcG7VA
dropbox-antifreeze-iH8IiP
exclude.txt
keepassxc-setevoy.lock
qtsingleapp-Viber-0-3e8-lockfile
...

И каталог бекапа:

tree /backups/test/ | grep excl
├── exclude.txt

ОК – каталога exclude_this нет, всё работает.

Возвращаемся к скрипту бекапа.

getopts()

Описание есть тут>>>.

Добавим две опции: -e, для указания exclude-файла, и -d – для запуска в --dry-run:

...
while getopts "e:d" opt; do
    case $opt in
        i) echo "Found Exclude option $opt"
           echo "Found  Exclude file for $opt - $OPTARG"
            ;;
        d) echo "Found Dry-Run option $opt"
            ;;
        *) echo "No reasonable options found!";;
    esac
done
...

Проверяем:

./home-backups.sh -e exclude.txt
Found Exclude option e
Found  Exclude file for e - exclude.txt
./home-backups.sh -d
Found Dry-Run option d

Теперь добавим переменные, по которым будем определять параметры:

...
EXCLUDE_FILE="/opt/home-backups-bash/exclude.txt"
DRY_RUN=

while getopts ":e:d" opt; do
    case $opt in
        e) EXCLUDE_FILE=$OPTARG
            ;;
        d) DRY_RUN="--dry-run"
            ;;
        *) echo "No reasonable options found!";;
    esac
done
...

EXCLUDE_FILE имеет дефолтное значение, которое можно переопределить с помощью -e.

DRY_RUN по умолчанию пустой, но если вызвать скрипт с -d – то сюда будет подставлено --dry-run.

И обновим функцию mkbackup():

...
mkbackup() {
    
    bkp_dir=$1
    home_dir=$2
    exclude_file=$3
    dry_run=$4

    sudo rsync $dry_run --exclude-from $exclude_file --archive --verbose $home_dir $bkp_dir
}
...

И её вызов:

...
if mkbackup $BKP_DIR $HOME_DIR $EXCLUDE_FILE $DRY_RUN; then
    notify "finished" $HOME_DIR
    echo finished
else
    echo failed
    notify "FAILED" $HOME_DIR
fi

Создаём файл /opt/home-backups-bash/exclude.txt с исключениями:

cat /opt/home-backups-bash/exclude.txt
.cache/
.directory
.dropbox/***
.dropbox-dist/
.esd_auth
.fehbg
.gmrun_history
.gnome2/
.gtkrc-2.0
.java/
.mono/
.mozilla/
.pki/
.PyCharmCE2018.2/
.rnd
.temp/
.wget-hsts
.Xauthority
.xneur

Запускаем, проверяем:

./home-backups.sh -d

.config в аутпуте нет, прочих каталогв и файлов из exclude.txt тоже – ОК.

Ещё можно добавить опцию -D – вызывать rsync с опцией --delete.

Повторяем всё тоже самое, как делали для --exclude-from и --dry-run:

...
EXCLUDE_FILE="/opt/home-backups-bash/exclude.txt"
DELETE=
DRY_RUN=

while getopts ":e:dD" opt; do
    case $opt in
        e) EXCLUDE_FILE=$OPTARG
            ;;
        d) DRY_RUN="--dry-run"
            ;;
        D) DELETE="--delete"
            ;;
        *) echo "No reasonable options found!"
            exit 1
            ;;
    esac
done
...
mkbackup() {
    
    bkp_dir=$1
    home_dir=$2
    exclude_file=$3
    dry_run=$4
    delete=$5

    sudo rsync $dry_run $delete --exclude-from $exclude_file --archive --verbose $home_dir $bkp_dir
}
...
if mkbackup $BKP_DIR $HOME_DIR $EXCLUDE_FILE $DRY_RUN $DELETE; then
    notify "finished" $HOME_DIR
    echo finished
else
    echo failed
    notify "FAILED" $HOME_DIR
fi

Проверяем с -d и -D:

./home-backups.sh -d -D

--delete сама по себе достаточно опасная опция, так что сначала лучше несколько ра запустить с -d для --dry-run, и проанализировать что будет удаляться.

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

Аналогично можно добавить опции, что бы переопределять любые другие параметры для скрипта – хомик юзера, каталог, куда складывать бекапы и прочее.