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

Автор: | 12/10/2018

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

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

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

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

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

[simterm]

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

[/simterm]

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

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

  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; }

Проверяем:

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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()

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

[simterm]

$ sudo mkdir /backups/Backups/Cron

[/simterm]

Обновляем скрипт – добавляем перменную 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 сессию пользователя:

[simterm]

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

[/simterm]

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

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

HOME_DIR="/home/setevoy/"


check-backups-fs() {

...

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

[simterm]

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

[/simterm]

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

[email protected]
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"

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

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

Лог:

[simterm]

$ 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)

[/simterm]

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

UPD

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

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

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

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

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

rsync() include и exclude

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

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

[simterm]

$ mkdir -p /tmp/include_this/dir1/dir2

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

$ 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

[/simterm]

Проверяем:

[simterm]

$ tree /backups/test/
/backups/test/
└── include_this
    ├── dir1
    │   ├── dir2
    │   │   └── file2
    │   └── file1
    └── file

3 directories, 3 files

[/simterm]

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

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

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

[simterm]

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

[/simterm]

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

[simterm]

$ cat exclude.txt 
exclude_this/***

[/simterm]

Проверяем rsync:

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

ОК – каталога 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
...

Проверяем:

[simterm]

$ ./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

[/simterm]

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

...
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 с исключениями:

[simterm]

$ 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

[/simterm]

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

[simterm]

$ ./home-backups.sh -d

[/simterm]

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

[simterm]

$ ./home-backups.sh -d -D

[/simterm]

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

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

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