像 macOS 的 Time Machine 一样备份您的 Linux 服务器(基于 rsync)


一、前言

Time Machine 这东西用过 macOS 的都知道,可以说是数据备份神器。

前几天我误删了几个文件,还好我有设置 Time Machine 自动备份,于是我打开 Time Machine,点几下鼠标,文件就回来了!

我想要是 Time Machine 能用来备份 Linux 服务器那该有多好啊,特别是像我这种喜欢乱改配置文件然后又从来不备份的人!

于是我 Google 了一下,发现 GitHub 上有一个基于 rsync 二次开发的备份脚本!于是乎马上开始折腾……

GitHub 项目:laurent22/rsync-time-backup

我的备份方式是外接硬盘。这个脚本我用了几天,感觉 Time Machine 的特性都具备了,而且磁盘满的时候可以自动清理最早的备份,权限、硬链接全部正常,觉得应该完美了。最重要的是(wo)不(bu)用(hui)配置复杂的 rsync,可以说是 Linux 版的 Time Machine!

二、系统环境

  • 操作系统:CentOS 7.3.1611(已关闭 SELinux)
  • 备份盘挂载点:/backup
  • 备份盘文件系统:ext4

三、配置步骤

3.1 下载脚本文件

克隆 GitHub 上的项目

git clone https://github.com/laurent22/rsync-time-backup.git

安装脚本文件

cp rsync-time-backup/rsync_tmbackup.sh /usr/local/bin/rsync_tmbackup.sh

赋予脚本执行权限

chmod +x /usr/local/bin/rsync_tmbackup.sh

附:脚本文件基本用法
rsync_tmbackup.sh <备份源> <备份目标> [排除文件列表]

3.2 创建备份排除列表

如果要备份根目录的话,必须指定一个排除列表来防止根目录所在分区以外分区的文件被备份。

文件格式:- <目录>(一行一个目录。)

下面是我的排除列表(位置:/etc/rsync_tmbackup_ext.txt):

cat /etc/rsync_tmbackup_ext.txt
- /mnt
- /backup
- /dev
- /proc
- /sys
- /tmp

3.3 在备份目标上创建一个标记点

在非 rsync 服务器上进行备份时,必须先创建一个标记点,否则备份将无法进行。使用 rsync 或者 scp 方式备份的可能不用(自己没试过)。

touch /backup/backup.marker

3.4 开始备份

/usr/local/bin/rsync_tmbackup.sh / /etc/rsync_tmbackup_ext.txt

不出意外的话,下面就开始刷屏了。

3.5 设置整点自动备份

crontab -e

请在命令行窗口中按下 O(大写) ,将以下内容直接粘贴到命令行窗口中,再按下 ESC ,最后输入 :wq 按回车。

0 */1 * * * /usr/bin/flock -n /tmp/backup.lock -c "/usr/local/bin/rsync_tmbackup.sh / /backup /etc/rsync_tmbackup_ext.txt" > /dev/null 2>&1

这一步完成之后,每隔一个小时系统就会自动执行该脚本来实现自动备份。

说明
• 请确保 crond 服务已开启,否则无法定时备份。

3.6 结果

下面是脚本运行几天之后我的备份目录。我这里设置的是每天 8 点到 22 点整点备份。

[root: ~]# ll /backup
总用量 80K
drwxr-xr-x  16 root root 4.0K 4月  29 15:00 .
dr-xr-xr-x. 20 root root 4.0K 4月  29 08:21 ..
dr-xr-xr-x   3 root root 4.0K 4月  29 14:04 2017-04-28-170001
dr-xr-xr-x  15 root root 4.0K 4月  26 22:11 2017-04-28-180002
dr-xr-xr-x  15 root root 4.0K 4月  26 22:11 2017-04-28-190001
dr-xr-xr-x  15 root root 4.0K 4月  26 22:11 2017-04-28-200001
dr-xr-xr-x  15 root root 4.0K 4月  26 22:11 2017-04-28-210001
dr-xr-xr-x  15 root root 4.0K 4月  26 22:11 2017-04-28-220001
dr-xr-xr-x  16 root root 4.0K 4月  29 08:21 2017-04-29-080002
dr-xr-xr-x  16 root root 4.0K 4月  29 08:21 2017-04-29-100002
dr-xr-xr-x  16 root root 4.0K 4月  29 08:21 2017-04-29-110003
dr-xr-xr-x  16 root root 4.0K 4月  29 08:21 2017-04-29-120002
dr-xr-xr-x  16 root root 4.0K 4月  29 08:21 2017-04-29-130001
dr-xr-xr-x  16 root root 4.0K 4月  29 08:21 2017-04-29-140002
dr-xr-xr-x  15 root root 4.0K 4月  29 08:21 2017-04-29-150002
-rw-r--r--   1 root root    6 4月  29 15:20 backup.inprogress
-rw-r--r--   1 root root    0 4月  27 07:59 backup.marker
lrwxrwxrwx   1 root root   17 4月  29 14:13 latest -> 2017-04-29-140002
drwx------   2 root root  16K 4月  26 22:10 lost+found

下面我们对比一下 macOS 原生的 Time Machine。

[zenandidi: ~]$ ls -l /Volumes/Time\ Machine/Backups.backupdb/余泽楠的MacBook\ Pro 
total 8
[email protected] 21 root  wheel   714B  4 29 15:03 .
[email protected]  6 root  wheel   204B  2  8 18:01 ..
[email protected]  6 root  wheel   204B  4 27 07:04 2017-04-27-070433
[email protected]  6 root  wheel   204B  4 28 09:21 2017-04-28-092124
[email protected]  6 root  wheel   204B  4 28 15:01 2017-04-28-150135
[email protected]  6 root  wheel   204B  4 28 16:00 2017-04-28-160028
[email protected]  6 root  wheel   204B  4 28 16:56 2017-04-28-165654
drw[email protected]  6 root  wheel   204B  4 28 17:55 2017-04-28-175554
[email protected]  6 root  wheel   204B  4 28 18:56 2017-04-28-185654
[email protected]  6 root  wheel   204B  4 28 20:08 2017-04-28-200806
[email protected]  6 root  wheel   204B  4 28 21:17 2017-04-28-211735
[email protected]  6 root  wheel   204B  4 28 22:45 2017-04-28-224546
[email protected]  6 root  wheel   204B  4 29 07:05 2017-04-29-070534
[email protected]  6 root  wheel   204B  4 29 08:05 2017-04-29-080531
[email protected]  6 root  wheel   204B  4 29 08:50 2017-04-29-085018
[email protected]  6 root  wheel   204B  4 29 09:54 2017-04-29-095453
[email protected]  6 root  wheel   204B  4 29 11:03 2017-04-29-110311
[email protected]  6 root  wheel   204B  4 29 13:04 2017-04-29-130419
[email protected]  6 root  wheel   204B  4 29 14:12 2017-04-29-141247
[email protected]  4 root  wheel   136B  4 29 15:03 2017-04-29-150331.inProgress
lrwxr-xr-x   1 root  wheel    17B  4 29 14:12 Latest -> 2017-04-29-141247

可以看出,仿真度已经非常高了!

3.7 恢复

这里恢复的话当然没有像 macOS 那样可以用漂亮的 GUI 啦!我们直接用 rsync 命令将备份时间点里面的文件恢复过去就好了。

例如恢复整个 /etc 目录:

rsync -aP /backup/2017-04-29-080002/etc/ /etc

我这里并没有尝试全盘恢复,全盘恢复的话用启动盘启动之后把全部文件拷贝过去应该也是可以的(如果开了 SELinux 那么还需要恢复一下安全上下文数据)。


四、注意事项

  1. 如果需要使用 samba 网络备份的话,必须在上面建立一个虚拟的磁盘映像,然后创建一个 ext4 文件系统再挂载,否则所有的硬链接将不可用,权限也无法保存。
  2. 如果要使用网络磁盘的话,推荐使用 NFS,或者直接备份到其他的 rsync 服务器上。
  3. 脚本可能会更新,更新之后的用法可能会改变,请以 GitHub 项目上的最新说明为准。

附:rsync-time-backup 脚本文件

(更新于 2017 年 12 月 9 日)

#!/usr/bin/env bash

APPNAME=$(basename $0 | sed "s/\.sh$//")

# -----------------------------------------------------------------------------
# Log functions
# -----------------------------------------------------------------------------

fn_log_info()  { echo "$APPNAME: $1"; }
fn_log_warn()  { echo "$APPNAME: [WARNING] $1" 1>&2; }
fn_log_error() { echo "$APPNAME: [ERROR] $1" 1>&2; }
fn_log_info_cmd()  {
    if [ -n "$SSH_DEST_FOLDER_PREFIX" ]; then
        echo "$APPNAME: $SSH_CMD '$1'";
    else
        echo "$APPNAME: $1";
    fi
}

# -----------------------------------------------------------------------------
# Make sure everything really stops when CTRL+C is pressed
# -----------------------------------------------------------------------------

fn_terminate_script() {
    fn_log_info "SIGINT caught."
    exit 1
}

trap 'fn_terminate_script' SIGINT

# -----------------------------------------------------------------------------
# Small utility functions for reducing code duplication
# -----------------------------------------------------------------------------
fn_display_usage() {
    echo "Usage: $(basename $0) [OPTION]... <[[email protected]:]SOURCE> <[[email protected]:]DESTINATION> [exclude-pattern-file]"
    echo ""
    echo "Options"
    echo " -p, --port           SSH port."
    echo " -h, --help           Display this help message."
    echo " --rsync-get-flags    Display the default rsync flags that are used for backup."
    echo " --rsync-set-flags    Set the rsync flags that are going to be used for backup."
    echo " --log-dir            Set the log file directory. If this flag is set, generated files will"
    echo "                      not be managed by the script - in particular they will not be"
    echo "                      automatically deleted."
    echo "                      Default: $LOG_DIR"
    echo ""
    echo "For more detailed help, please see the README file:"
    echo ""
    echo "https://github.com/laurent22/rsync-time-backup/blob/master/README.md"
}

fn_parse_date() {
    # Converts YYYY-MM-DD-HHMMSS to YYYY-MM-DD HH:MM:SS and then to Unix Epoch.
    case "$OSTYPE" in
        linux*) date -d "${1:0:10} ${1:11:2}:${1:13:2}:${1:15:2}" +%s ;;
        cygwin*) date -d "${1:0:10} ${1:11:2}:${1:13:2}:${1:15:2}" +%s ;;
        darwin8*) yy=`expr ${1:0:4}`
            mm=`expr ${1:5:2} - 1`
            dd=`expr ${1:8:2}`
            hh=`expr ${1:11:2}`
            mi=`expr ${1:13:2}`
            ss=`expr ${1:15:2}`
            # Because under MacOS X Tiger 'date -j' doesn't work, we do this:
            perl -e 'use Time::Local; print timelocal('$ss','$mi','$hh','$dd','$mm','$yy'),"\n";' ;;
        darwin*) date -j -f "%Y-%m-%d-%H%M%S" "$1" "+%s" ;;
        FreeBSD*) date -j -f "%Y-%m-%d-%H%M%S" "$1" "+%s" ;;
    esac
}

fn_find_backups() {
    fn_run_cmd "find "$DEST_FOLDER" -maxdepth 1 -type d -name \"????-??-??-??????\" -prune | sort -r"
}

fn_expire_backup() {
    # Double-check that we're on a backup destination to be completely
    # sure we're deleting the right folder
    if [ -z "$(fn_find_backup_marker "$(dirname -- "$1")")" ]; then
        fn_log_error "$1 is not on a backup destination - aborting."
        exit 1
    fi

    fn_log_info "Expiring $1"
    fn_rm_dir "$1"
}

fn_parse_ssh() {
    # To keep compatibility with bash version < 3, we use grep
    if echo "$DEST_FOLDER"|grep -Eq '^[A-Za-z0-9\._%\+\-][email protected][A-Za-z0-9.\-]+\:.+$'
    then
        SSH_USER=$(echo "$DEST_FOLDER" | sed -E  's/^([A-Za-z0-9\._%\+\-]+)@([A-Za-z0-9.\-]+)\:(.+)$/\1/')
        SSH_HOST=$(echo "$DEST_FOLDER" | sed -E  's/^([A-Za-z0-9\._%\+\-]+)@([A-Za-z0-9.\-]+)\:(.+)$/\2/')
        SSH_DEST_FOLDER=$(echo "$DEST_FOLDER" | sed -E  's/^([A-Za-z0-9\._%\+\-]+)@([A-Za-z0-9.\-]+)\:(.+)$/\3/')
        SSH_CMD="ssh -p $SSH_PORT ${SSH_USER}@${SSH_HOST}"
        SSH_DEST_FOLDER_PREFIX="${SSH_USER}@${SSH_HOST}:"
    elif echo "$SRC_FOLDER"|grep -Eq '^[A-Za-z0-9\._%\+\-][email protected][A-Za-z0-9.\-]+\:.+$'
    then
        SSH_USER=$(echo "$SRC_FOLDER" | sed -E  's/^([A-Za-z0-9\._%\+\-]+)@([A-Za-z0-9.\-]+)\:(.+)$/\1/')
        SSH_HOST=$(echo "$SRC_FOLDER" | sed -E  's/^([A-Za-z0-9\._%\+\-]+)@([A-Za-z0-9.\-]+)\:(.+)$/\2/')
        SSH_SRC_FOLDER=$(echo "$SRC_FOLDER" | sed -E  's/^([A-Za-z0-9\._%\+\-]+)@([A-Za-z0-9.\-]+)\:(.+)$/\3/')
        SSH_CMD="ssh -p $SSH_PORT ${SSH_USER}@${SSH_HOST}"
        SSH_SRC_FOLDER_PREFIX="${SSH_USER}@${SSH_HOST}:"
    fi
}

fn_run_cmd() {
    if [ -n "$SSH_DEST_FOLDER_PREFIX" ] 
    then
        eval "$SSH_CMD '$1'"
    else
        eval $1
    fi
}

fn_find() {
    fn_run_cmd "find '$1'"  2>/dev/null
}

fn_get_absolute_path() {
    fn_run_cmd "cd '$1';pwd"
}

fn_mkdir() {
    fn_run_cmd "mkdir -p -- '$1'"
}

# Removes a file or symlink - not for directories
fn_rm_file() {
    fn_run_cmd "rm -f -- '$1'"
}

fn_rm_dir() {
    fn_run_cmd "rm -rf -- '$1'"
}

fn_touch() {
    fn_run_cmd "touch -- '$1'"
}

fn_ln() {
    fn_run_cmd "ln -s -- '$1' '$2'"
}

# -----------------------------------------------------------------------------
# Source and destination information
# -----------------------------------------------------------------------------
SSH_USER=""
SSH_HOST=""
SSH_DEST_FOLDER=""
SSH_SRC_FOLDER=""
SSH_CMD=""
SSH_DEST_FOLDER_PREFIX=""
SSH_SRC_FOLDER_PREFIX=""
SSH_PORT="22"

SRC_FOLDER=""
DEST_FOLDER=""
EXCLUSION_FILE=""
LOG_DIR="$HOME/.$APPNAME"
AUTO_DELETE_LOG="1"

RSYNC_FLAGS="-D --compress --numeric-ids --links --hard-links --one-file-system --itemize-changes --times --recursive --perms --owner --group --stats --human-readable"

while :; do
    case $1 in
        -h|-\?|--help)
            fn_display_usage
            exit
            ;;
        -p|--port)
            shift
            SSH_PORT=$1
            ;;
        --rsync-get-flags)
            shift
            echo $RSYNC_FLAGS
            exit
            ;;
        --rsync-set-flags)
            shift
            RSYNC_FLAGS="$1"
            ;;
        --log-dir)
            shift
            LOG_DIR="$1"
            AUTO_DELETE_LOG="0"
            ;;
        --)
            shift
            SRC_FOLDER="$1"
            DEST_FOLDER="$2"
            EXCLUSION_FILE="$3"
            break
            ;;
        -*)
            fn_log_error "Unknown option: \"$1\""
            fn_log_info ""
            fn_display_usage
            exit 1
            ;;
        *)
            SRC_FOLDER="$1"
            DEST_FOLDER="$2"
            EXCLUSION_FILE="$3"
            break
    esac

    shift
done

# Display usage information if required arguments are not passed
if [[ -z "$SRC_FOLDER" || -z "$DEST_FOLDER" ]]; then
    fn_display_usage
    exit 1
fi

# Strips off last slash from dest. Note that it means the root folder "/"
# will be represented as an empty string "", which is fine
# with the current script (since a "/" is added when needed)
# but still something to keep in mind.
# However, due to this behavior we delay stripping the last slash for
# the source folder until after parsing for ssh usage.

DEST_FOLDER="${DEST_FOLDER%/}"

fn_parse_ssh

if [ -n "$SSH_DEST_FOLDER" ]; then
    DEST_FOLDER="$SSH_DEST_FOLDER"
fi

if [ -n "$SSH_SRC_FOLDER" ]; then
    SRC_FOLDER="$SSH_SRC_FOLDER"
fi

# Now strip off last slash from source folder.
SRC_FOLDER="${SRC_FOLDER%/}"

for ARG in "$SRC_FOLDER" "$DEST_FOLDER" "$EXCLUSION_FILE"; do
    if [[ "$ARG" == *"'"* ]]; then
        fn_log_error 'Source and destination directories may not contain single quote characters.'
        exit 1
    fi
done

# -----------------------------------------------------------------------------
# Check that the destination drive is a backup drive
# -----------------------------------------------------------------------------

# TODO: check that the destination supports hard links

fn_backup_marker_path() { echo "$1/backup.marker"; }
fn_find_backup_marker() { fn_find "$(fn_backup_marker_path "$1")" 2>/dev/null; }

if [ -z "$(fn_find_backup_marker "$DEST_FOLDER")" ]; then
    fn_log_info "Safety check failed - the destination does not appear to be a backup folder or drive (marker file not found)."
    fn_log_info "If it is indeed a backup folder, you may add the marker file by running the following command:"
    fn_log_info ""
    fn_log_info_cmd "mkdir -p -- \"$DEST_FOLDER\" ; touch \"$(fn_backup_marker_path "$DEST_FOLDER")\""
    fn_log_info ""
    exit 1
fi

# -----------------------------------------------------------------------------
# Setup additional variables
# -----------------------------------------------------------------------------

# Date logic
NOW=$(date +"%Y-%m-%d-%H%M%S")
EPOCH=$(date "+%s")
KEEP_ALL_DATE=$((EPOCH - 86400))       # 1 day ago
KEEP_DAILIES_DATE=$((EPOCH - 2678400)) # 31 days ago

export IFS=$'\n' # Better for handling spaces in filenames.
DEST="$DEST_FOLDER/$NOW"
PREVIOUS_DEST="$(fn_find_backups | head -n 1)"
INPROGRESS_FILE="$DEST_FOLDER/backup.inprogress"
MYPID="$$"

# -----------------------------------------------------------------------------
# Create log folder if it doesn't exist
# -----------------------------------------------------------------------------

if [ ! -d "$LOG_DIR" ]; then
    fn_log_info "Creating log folder in '$LOG_DIR'..."
    mkdir -- "$LOG_DIR"
fi

# -----------------------------------------------------------------------------
# Handle case where a previous backup failed or was interrupted.
# -----------------------------------------------------------------------------

if [ -n "$(fn_find "$INPROGRESS_FILE")" ]; then
    if [ "$OSTYPE" == "cygwin" ]; then
        # 1. Grab the PID of previous run from the PID file
        RUNNINGPID="$(fn_run_cmd "cat $INPROGRESS_FILE")"

        # 2. Get the command for the process currently running under that PID and look for our script name
        RUNNINGCMD="$(procps -wwfo cmd -p $RUNNINGPID --no-headers | grep "$APPNAME")"

        # 3. Grab the exit code from grep (0=found, 1=not found)
        GREPCODE=$?

        # 4. if found, assume backup is still running
        if [ "$GREPCODE" = 0 ]; then
            fn_log_error "Previous backup task is still active - aborting (command: $RUNNINGCMD)."
            exit 1
        fi
    else 
        RUNNINGPID="$(fn_run_cmd "cat $INPROGRESS_FILE")"
        if [ "$RUNNINGPID" = "$(pgrep "$APPNAME")" ]; then
            fn_log_error "Previous backup task is still active - aborting."
            exit 1
        fi
    fi

    if [ -n "$PREVIOUS_DEST" ]; then
        # - Last backup is moved to current backup folder so that it can be resumed.
        # - 2nd to last backup becomes last backup.
        fn_log_info "$SSH_DEST_FOLDER_PREFIX$INPROGRESS_FILE already exists - the previous backup failed or was interrupted. Backup will resume from there."
        fn_run_cmd "mv -- $PREVIOUS_DEST $DEST"
        if [ "$(fn_find_backups | wc -l)" -gt 1 ]; then
            PREVIOUS_DEST="$(fn_find_backups | sed -n '2p')"
        else
            PREVIOUS_DEST=""
        fi
        # update PID to current process to avoid multiple concurrent resumes
        fn_run_cmd "echo $MYPID > $INPROGRESS_FILE"
    fi
fi

# Run in a loop to handle the "No space left on device" logic.
while : ; do

    # -----------------------------------------------------------------------------
    # Check if we are doing an incremental backup (if previous backup exists).
    # -----------------------------------------------------------------------------

    LINK_DEST_OPTION=""
    if [ -z "$PREVIOUS_DEST" ]; then
        fn_log_info "No previous backup - creating new one."
    else
        # If the path is relative, it needs to be relative to the destination. To keep
        # it simple, just use an absolute path. See http://serverfault.com/a/210058/118679
        PREVIOUS_DEST="$(fn_get_absolute_path "$PREVIOUS_DEST")"
        fn_log_info "Previous backup found - doing incremental backup from $SSH_DEST_FOLDER_PREFIX$PREVIOUS_DEST"
        LINK_DEST_OPTION="--link-dest='$PREVIOUS_DEST'"
    fi

    # -----------------------------------------------------------------------------
    # Create destination folder if it doesn't already exists
    # -----------------------------------------------------------------------------

    if [ -z "$(fn_find "$DEST -type d" 2>/dev/null)" ]; then
        fn_log_info "Creating destination $SSH_DEST_FOLDER_PREFIX$DEST"
        fn_mkdir "$DEST"
    fi

    # -----------------------------------------------------------------------------
    # Purge certain old backups before beginning new backup.
    # -----------------------------------------------------------------------------

    # Default value for $PREV ensures that the most recent backup is never deleted.
    PREV="0000-00-00-000000"
    for FILENAME in $(fn_find_backups | sort -r); do
        BACKUP_DATE=$(basename "$FILENAME")
        TIMESTAMP=$(fn_parse_date $BACKUP_DATE)

        # Skip if failed to parse date...
        if [ -z "$TIMESTAMP" ]; then
            fn_log_warn "Could not parse date: $FILENAME"
            continue
        fi

        if   [ $TIMESTAMP -ge $KEEP_ALL_DATE ]; then
            true
        elif [ $TIMESTAMP -ge $KEEP_DAILIES_DATE ]; then
            # Delete all but the most recent of each day.
            [ "${BACKUP_DATE:0:10}" == "${PREV:0:10}" ] && fn_expire_backup "$FILENAME"
        else
            # Delete all but the most recent of each month.
            [ "${BACKUP_DATE:0:7}" == "${PREV:0:7}" ] && fn_expire_backup "$FILENAME"
        fi

        PREV=$BACKUP_DATE
    done

    # -----------------------------------------------------------------------------
    # Start backup
    # -----------------------------------------------------------------------------

    LOG_FILE="$LOG_DIR/$(date +"%Y-%m-%d-%H%M%S").log"

    fn_log_info "Starting backup..."
    fn_log_info "From: $SSH_SRC_FOLDER_PREFIX$SRC_FOLDER/"
    fn_log_info "To:   $SSH_DEST_FOLDER_PREFIX$DEST/"

    CMD="rsync"
    if [ -n "$SSH_CMD" ]; then
        CMD="$CMD  -e 'ssh -p $SSH_PORT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'"
    fi
    CMD="$CMD $RSYNC_FLAGS"
    CMD="$CMD --log-file '$LOG_FILE'"
    if [ -n "$EXCLUSION_FILE" ]; then
        # We've already checked that $EXCLUSION_FILE doesn't contain a single quote
        CMD="$CMD --exclude-from '$EXCLUSION_FILE'"
    fi
    CMD="$CMD $LINK_DEST_OPTION"
    CMD="$CMD -- '$SSH_SRC_FOLDER_PREFIX$SRC_FOLDER/' '$SSH_DEST_FOLDER_PREFIX$DEST/'"

    fn_log_info "Running command:"
    fn_log_info "$CMD"

    fn_run_cmd "echo $MYPID > $INPROGRESS_FILE"
    eval $CMD

    # -----------------------------------------------------------------------------
    # Check if we ran out of space
    # -----------------------------------------------------------------------------

    NO_SPACE_LEFT="$(grep "No space left on device (28)\|Result too large (34)" "$LOG_FILE")"

    if [ -n "$NO_SPACE_LEFT" ]; then
        fn_log_warn "No space left on device - removing oldest backup and resuming."

        if [[ "$(fn_find_backups | wc -l)" -lt "2" ]]; then
            fn_log_error "No space left on device, and no old backup to delete."
            exit 1
        fi

        fn_expire_backup "$(fn_find_backups | tail -n 1)"

        # Resume backup
        continue
    fi

    # -----------------------------------------------------------------------------
    # Check whether rsync reported any errors
    # -----------------------------------------------------------------------------

    EXIT_CODE="1"
    if [ -n "$(grep "rsync error:" "$LOG_FILE")" ]; then
        fn_log_error "Rsync reported an error. Run this command for more details: grep -E 'rsync:|rsync error:' '$LOG_FILE'"
    elif [ -n "$(grep "rsync:" "$LOG_FILE")" ]; then
        fn_log_warn "Rsync reported a warning. Run this command for more details: grep -E 'rsync:|rsync error:' '$LOG_FILE'"
    else
        fn_log_info "Backup completed without errors."
        if [[ $AUTO_DELETE_LOG == "1" ]]; then
            rm -f -- "$LOG_FILE"
        fi
        EXIT_CODE="0"
    fi

    # -----------------------------------------------------------------------------
    # Add symlink to last backup
    # -----------------------------------------------------------------------------

    fn_rm_file "$DEST_FOLDER/latest"
    fn_ln "$(basename -- "$DEST")" "$DEST_FOLDER/latest"

    fn_rm_file "$INPROGRESS_FILE"

    exit $EXIT_CODE
done

发表评论

电子邮件地址不会被公开。