简介

之前我的博客和服务器上的其他系统都是我手动备份数据的,因为懒,所以懒得整自动备份

最近连手动备份也懒了,想想还是整个自动备份吧

我使用的工具是restic是本地/远程备份工具,可以实现增量备份 + 去重 + 压缩 + 加密 + 快照管理

可以把数据备份到 本地磁盘、NAS、SFTP、S3 等支持的后端

功能 描述
增量备份 每次只保存变化的数据,节省空间
去重 不同备份间重复数据只保存一次
压缩 数据存储时自动压缩,节省存储空间
加密 备份自动加密,保证安全
快照管理 可以查看、保留、删除历史版本
跨平台 Linux / Windows / macOS 都支持


思路

我的服务器使用的是联想M73,只有一个硬盘位,所以打算外接一个USB硬盘来存储备份文件

刚开始想使用网盘进行备份的,后面发现很多网盘备份工具都不知此个人版的网盘,只支持企业版,当然也不是没有办法,先把网盘挂载成webdav,然后存进去就可以了

但是我懒,反正家里面有硬盘



第一步:格式化硬盘

首先把USB硬盘插上去,然后使用下面命令查看硬盘信息,可以看到这里已经读出我的硬盘了,是/dev/sdc1

lsblk -f

1.png

硬盘格式是ntfs,这个格式对linux并不友好,我需要格式化成ext4格式

首先卸载硬盘,如果没有挂载的话就忽略这一步,如果不知道硬盘有没有挂载就直接执行也没事,没有挂载会有提示

umount /dev/sdc1

2.png

然后格式化硬盘,中间可能会弹出提示,输入y回车继续

mkfs.ext4 -L backup /dev/sdc1

3.png

等待完成

4.png



第二步:挂载硬盘

使用下面命令查看确定硬盘的UUID,记得把UUID复制下来

blkid /dev/sdc1

5.png

使用vi编辑下面文件

vi /etc/fstab

6.png

在文件最后面添加挂载信息,把下面这一行复制修改UUID为你硬盘的UUID,ntfs-3g改成硬盘格式,然后粘贴到文件的最后面,因为是移动硬盘,所以我们加了nofail,x-systemd.device-timeout=10,这个是开机等待10秒,不管有没有检测到这个设备都直接开机,如果不加的话移动硬盘出现问题挂载不了就会导致开机卡住,开不了机

UUID=068273068272FA0F   /mnt/usb   ntfs-3g   defaults,nofail,x-systemd.device-timeout=10   0   0

7.png

添加完成后,保存退出

8.png

执行立即挂载命令,但是我这提示挂载失败,挂载点不存在

mount -a

9.png

这个是因为我的系统里面没有/mnt/usb这个目录,所以挂载不上,我先创建一个/mnt/usb目录

mkdir /mnt/usb

10.png

然后再次执行挂载命令,提示不是错误,只是 systemd 在提醒:你修改了 /etc/fstab,但 systemd 还缓存着旧配置。 不过 mount -a 是直接读 /etc/fstab 的,不依赖 systemd,所以已经生效了

mount -a

11.png

使用下面命令查看是否挂载成功,看到有挂载点就是成功了

lsblk -f

12.png

使用下面命令让 systemd 下次启动时用新的配置

systemctl daemon-reload

13.png



第三步:下载release

release可以直接使用系统源yum、apt下载,但是版本比较老,我是直接去github项目地址下载的

项目地址:https://github.com/restic/restic/

首先创建一个文件夹,用来存放release的二进制运行文件

mkdir /opt/release

14.png

进入刚刚创建的文件夹

cd /opt/release

15.png

进入release项目地址

16.png

点击顶部的标签

17.png

点击最新的版本号

18.png

滑倒下面,找到对应自己服务器操作系统和处理器的版本,右键,复制链接地址

19.png

回到SSH终端,使用wget命令下载

wget https://github.com/restic/restic/releases/download/v0.18.1/restic_0.18.1_linux_amd64.bz2

20.png

下载完成后解压出来,然后使用ls命令查看解压出来的文件

bunzip2 restic_0.18.1_linux_amd64.bz2
ls

21.png

解压出来的文件名太长了,我们重命名一下

mv restic_0.18.1_linux_amd64 restic

22.png

修改文件权限,让文件可执行

chmod +x restic

23.png



第四步:创建备份文件列表

使用vi编辑器创建一个txt文件,名称为:backup_dirs.txt

vi backup_dirs.txt

24.png

然后把需要备份的文件目录写进去,这样后面我们需要增加或者删除备份目录就直接修改这个文件列表即可

/home/wwwroot/www.ersansi.top
/root/Dufs-data
/usr/local/frpc
/usr/local/hysteria2
/root/mysql-bak-up

25.png

编辑完成后保存并退出

26.png



第五步:初始化备份仓库

我们先在刚刚挂载的USB移动硬盘上创建一个文件夹,用于存放备份文件

mkdir -p /mnt/usb/restic-repo

27.png

在创建一个密码文件,用于存储备份加密密码,这个密码很重要,恢复备份的时候也需要这个密码

echo "你的备份密码" > /opt/release/.restic-passwd

28.png

修改文件权限

chmod 600 .restic-passwd

29.png

初始化仓库,看到下图的结果就是初始化成功了

/opt/release/restic -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd init

30.png



第六步:测试备份

使用下面命令测试一下是否可以正常备份,backup后面就是你需要备份的文件目录路径

/opt/release/restic -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd backup 你的需要备份的文件路径

/opt/release/restic -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd backup /usr/local/frpc

看最后一行,快照ID是1c0a9736,说明备份成功了

31.png

查看一下快照内容

/opt/release/restic -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd snapshots

32.png

测试还原

/opt/release/restic -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd restore 快照ID --target 需要还原到的目录

/opt/release/restic -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd restore 1c0a9736 --target /root/test-restore

可以看到也是可以正常还原的

33.png



第七步:创建脚本

因为我的备份有mysql,我需要先把数据库导出来,然后再进行备份,所以我需要一个脚本来导出数据库,数据库导出成功后才能备份

首先创建一个.sh文件

vi full-backup.sh

然后把下面内容复制修改一下粘贴到里面

#!/bin/bash
# -------------------------------
# 全量备份 MySQL + 文件到 Restic
# -------------------------------

# ---------------- MySQL 配置 ----------------
MYSQL_USER="数据库用户名"
MYSQL_PASSWORD="数据库密码"
MYSQL_HOST="数据库地址"
MYSQL_BACKUP_DIR="导出到的文件目录路径"
DB_TO_BACKUP="需要导出的数据库,需要导出所有则去掉引号输入0"
EMAIL="接收邮件的邮箱,需要安装对应服务才能使用发送邮件功能"

# ---------------- Restic 配置 ----------------
RESTIC_BIN="restic二进制文件路径"
RESTIC_REPO="需要备份到的仓库路径"
RESTIC_PASSWORD_FILE="密码文件路径"
BACKUP_DIRS_FILE="备份列表路径"

mkdir -p "$MYSQL_BACKUP_DIR"

send_mail() {
    local subject="$1"
    local body="$2"
    echo -e "$body" | mail -s "$subject" -r "服务器备份 <发送邮件的邮箱>" "$EMAIL"
}

# ---------------- 1. MySQL 备份 ----------------
if [ "$DB_TO_BACKUP" == "0" ]; then
    DBS=$(/usr/bin/mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -h "$MYSQL_HOST" -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|mysql|sys)")
else
    DBS="$DB_TO_BACKUP"
fi

for db in $DBS; do
    echo "备份数据库: $db"
    if ! /usr/bin/mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -h "$MYSQL_HOST" "$db" > "$MYSQL_BACKUP_DIR/$db.sql"; then
        echo "[$(date)] 数据库 $db 导出失败" >&2
        send_mail "MySQL 备份失败" "数据库 $db 导出失败,请检查服务器"
        exit 1
    fi
done

echo "MySQL 备份成功: $(date)"

# ---------------- 2. Restic 备份 ----------------
echo "开始 Restic 备份..."
if ! $RESTIC_BIN -r "$RESTIC_REPO" --password-file "$RESTIC_PASSWORD_FILE" backup --files-from "$BACKUP_DIRS_FILE"; then
    echo "[$(date)] Restic 备份失败" >&2
    send_mail "Restic 备份失败" "文件备份失败,请检查服务器"
    exit 1
fi

echo "Restic 备份完成: $(date)"
send_mail "备份成功" "MySQL + 文件备份成功完成!"

34.png

编辑完成后保存退出,然后授权文件可执行

chmod +x full-backup.sh

35.png

执行测试一下是否可用

./full-backup.sh

36.png



第七步:创建服务

测试脚本成功了,接下来就是创建system的任务文件了

vi /etc/systemd/system/full-backup.service

37.png

把下面内容复制粘贴进去,记得把路径改成自己脚本所在的路径

[Unit]
Description=全量备份 MySQL + 文件
After=network.target

[Service]
Type=oneshot
ExecStart=/opt/release/full-backup.sh
StandardOutput=journal
StandardError=journal
RemainAfterExit=no

[Install]
WantedBy=multi-user.target

38.png

编辑完成后保存并退出,然后修改权限为可执行权限

39.png



第八步:创建定时器

上面创建了服务,用来运行sh脚本,但是我们还需要一个定时器来触发服务,这样就可以实现再指定的时间断进行备份了

vi /etc/systemd/system/full-backup.timer

40.png

然后把下面内容复制粘贴进去

[Unit]
Description=每天凌晨 3 点执行全量备份

[Timer]
Unit=full-backup.service
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

41.png

OnCalendar=-- 03:00:00 表示每天的 03:00:00 运行(-- 代表任意年-月-日)

Persistent=true 如果机器在 03:00 没开机,开机后会立即补跑一次(防止错过)

编辑好后保存并退出,然后修改权限为可执行

chmod +x /etc/systemd/system/full-backup.timer

42.png



第九步:应用配置

所有文件全部配置好后,我们就可以应用配置了

首先重载一下systemd配置文件

systemctl daemon-reload

43.png

然后设置定时器开机就启动

systemctl enable full-backup.timer

44.png

最后手动启动定时器

systemctl start full-backup.timer

45.png

启动完成后我们可以执行下面命令查看定时任务

systemctl list-timers --all

46.png



restic命令

为了方便理解,我这里列一下文件路径

restic二进制文件路径:/opt/release/restic

仓库路径:/mnt/usb/restic-repo

密码文件路径:/opt/release/.restic-passwd

备份列表文件路径:/opt/release/backup_dirs.txt

快照ID:1d0ce954

恢复目标目录:/root/bakup

快照挂载点:/usr/local/frpc


初始化仓库

第一次备份的时候需要先初始化仓库,后面不需要

restic路径 init -r 仓库路径 --password-file 密码文件路径

套入我上面的路径就是下面命令

/opt/release/restic init -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

备份目录

直接指定备份目录:

restic路径 backup 需要备份的目录 -r 仓库路径 --password-file 密码文件路径

指定备份文件列表文件:

restic路径 backup --files-from 列表文件路径 -r 仓库路径 --password-file 密码文件路径

替换成我上面的文件文件路径

/opt/release/restic backup --files-from /opt/release/backup_dirs.txt -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

列出所有快照

restic路径 snapshots -r 仓库路径 --password-file 密码文件路径

替换成我上面的路径

/opt/release/restic snapshots -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

查看仓库状态

restic路径 stats -r 仓库路径 --password-file 密码文件路径

替换成我上面的路径

/opt/release/restic stats -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

查看快照详细文件列表

restic路径 ls 快照ID -r /仓库路径 --password-file /密码文件路径

替换成我上面的路径

/opt/release/restic ls 1d0ce954 -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

恢复快照到指定目录

restic路径 restore 快照ID -r 仓库路径 --password-file 密码文件路径 --target 恢复目标目录

替换成我上面的路径

/opt/release/restic restore 1d0ce954 -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd --target /root/bakup

恢复指定快照中的指定目录

例如我现在备份仓库中有很多目录,我只需要恢复其中一个目录,不需要恢复整个快照,那是可以直接指定恢复的目录的

下面示例是我只需要恢复快照id为1d0ce954的里面的etc目录

restic路径 restore 快照ID -r 仓库路径 --password-file 密码文件路径 --target 需要恢复到的路径 --include 需要恢复的目录

替换成上面的路径:

/opt/release/restic restore 快照ID -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd --target /tmp/restore --include /etc

恢复指定快照中的指定文件

restic路径 restore 快照ID -r 仓库路径 --password-file 密码文件路径 --target 需要恢复到的路径 --include 需要恢复的文件路径

只把 /etc下的hosts 恢复到 /tmp/restore/etc

/opt/release/restic restore 1d0ce954 -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd --target /tmp/restore --include /etc/hosts

排除一些目录,只恢复其他内容

restic路径 restore 快照ID -r 仓库路径 --password-file 密码文件路径 --target 需要恢复到的路径 --include 需要恢复的目录 --exclude 需要排除的目录路径

比如只恢复 /home,但不要 /home/cache

/opt/release/restic restore d0ce954 -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd --target /tmp/restore --include /home --exclude /home/cache

清理未引用的数据(垃圾回收)

restic路径 forget --prune -r 仓库路径 --password-file 密码文件路径

替换成上面路径

/opt/release/restic forget --prune -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

设置保留策略(例如:保留最近 7 天的备份)

restic路径 forget --keep-daily 7 --prune -r 仓库路径 --password-file 密码文件路径

替换成上面路径:

/opt/release/restic forget --keep-daily 7 --prune -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

校验备份完整性

restic路径 check -r 仓库路径 --password-file 密码文件路径

替换成上面路径:

/opt/release/restic check -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

挂载快照(FUSE,需要 Linux)

restic路径 mount 挂载点 -r 仓库路径 --password-file 密码文件路径

替换成上面路径:

/opt/release/restic mount /usr/local/frpc -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd

删除指定快照

restic路径 -r 仓库路径 forget <快照ID1> --prune --password-file 密码文件路径

替换成上面路径

/opt/release/restic -r /mnt/usb/restic-repo forget 1d0ce954 --prune --password-file /opt/release/.restic-passwd

如果有多个快照ID则用空格隔开



restic菜单脚本

我还做了一个菜单脚本,方便使用restic,需要的可以下载把里面的环境参数修改一下即可使用

47.png

下面是菜单脚本的内容,记得把上面的环境变量修改和自己相符的

#!/bin/bash

# 配置
RESTIC_BIN="restic二进制文件路径"
REPO="仓库路径"
PASSFILE="密码文件路径"
RESTORE_TARGET="默认的快照恢复目标目录"
DEFAULT_MOUNT="默认挂载快照的目录"
FULL_BACKUP="备份脚本路径"

# 检查 restic 和 full-backup.sh 是否存在
if [ ! -x "$RESTIC_BIN" ]; then
    echo "错误: Restic 未找到,请检查路径: $RESTIC_BIN"
    exit 1
fi
if [ ! -x "$FULL_BACKUP" ]; then
    echo "错误: full-backup.sh 未找到或不可执行: $FULL_BACKUP"
    exit 1
fi

# 通用暂停函数
function pause() {
    echo
    read -p "按 Enter 返回菜单..."
}

# 显示快照列表
function show_snapshots() {
    clear
    echo "=== 快照列表 ==="
    $RESTIC_BIN snapshots -r $REPO --password-file $PASSFILE | less -SFXR
    echo "================"
    pause
}

# 显示快照文件列表
function show_snapshot_files() {
    show_snapshots
    read -p "请输入快照ID查看文件: " SNAPSHOT_ID
    clear
    echo "=== 快照 $SNAPSHOT_ID 文件列表 ==="
    $RESTIC_BIN ls $SNAPSHOT_ID -r $REPO --password-file $PASSFILE | less -SFXR
    echo "================"
    pause
}

# 菜单循环
while true; do
    clear
    echo "====================================="
    echo "         Restic 备份管理菜单"
    echo "  仓库: $REPO"
    echo "  密码文件: $PASSFILE"
    echo "====================================="
    echo " 1) 初始化仓库"
    echo " 2) 执行完整备份 (full-backup.sh)"
    echo " 3) 查看快照列表"
    echo " 4) 查看仓库状态"
    echo " 5) 查看快照文件内容"
    echo " 6) 恢复快照 (支持指定目录/文件)"
    echo " 7) 删除旧快照并清理"
    echo " 8) 校验仓库完整性"
    echo " 9) 挂载快照 (浏览文件)"
    echo "10) 卸载快照"
    echo "11) 手动执行 forget/keep 策略"
    echo " 0) 退出"
    echo "====================================="
    read -p "请选择操作: " choice

    case $choice in
        1)
            echo "⚠️ 初始化仓库会清空原有数据,仅首次使用时需要执行!"
            read -p "确认初始化仓库?(yes/no): " confirm
            if [ "$confirm" == "yes" ]; then
                $RESTIC_BIN init -r $REPO --password-file $PASSFILE
            else
                echo "已取消。"
            fi
            pause
            ;;
        2)
            echo "开始完整备份..."
            if "$FULL_BACKUP"; then
                echo "备份完成。"
            else
                echo "备份失败,请检查 full-backup.sh 日志。"
            fi
            pause
            ;;
        3)
            show_snapshots
            ;;
        4)
            clear
            echo "=== 仓库状态 ==="
            $RESTIC_BIN stats -r $REPO --password-file $PASSFILE | less -SFXR
            echo "================"
            pause
            ;;
        5)
            show_snapshot_files
            ;;
        6)
            show_snapshots
            read -p "请输入快照ID: " SNAPSHOT_ID
            read -p "请输入要恢复的路径(例如 /etc,留空恢复全部): " INCLUDE_PATH
            read -p "请输入恢复目标目录(默认 $RESTORE_TARGET): " TARGET
            TARGET=${TARGET:-$RESTORE_TARGET}
            mkdir -p "$TARGET"
            if [ -z "$INCLUDE_PATH" ]; then
                $RESTIC_BIN restore $SNAPSHOT_ID -r $REPO --password-file $PASSFILE --target "$TARGET"
            else
                $RESTIC_BIN restore $SNAPSHOT_ID -r $REPO --password-file $PASSFILE --target "$TARGET" --include "$INCLUDE_PATH"
            fi
            echo "恢复完成,文件在: $TARGET"
            pause
            ;;
        7)
            read -p "请输入保留的天数(默认7): " DAYS
            DAYS=${DAYS:-7}
            echo "模拟执行: 将显示哪些快照会被删除 (dry-run)"
            $RESTIC_BIN forget --keep-daily $DAYS --prune --dry-run -r $REPO --password-file $PASSFILE | less -SFXR
            read -p "确认执行删除?(yes/no): " CONFIRM
            if [ "$CONFIRM" == "yes" ]; then
                $RESTIC_BIN forget --keep-daily $DAYS --prune -r $REPO --password-file $PASSFILE
                echo "清理完成。"
            else
                echo "已取消清理。"
            fi
            pause
            ;;
        8)
            clear
            echo "校验仓库完整性..."
            $RESTIC_BIN check -r $REPO --password-file $PASSFILE | less -SFXR
            pause
            ;;
        9)
            read -p "请输入挂载目录(默认 $DEFAULT_MOUNT): " MOUNT_DIR
            MOUNT_DIR=${MOUNT_DIR:-$DEFAULT_MOUNT}
            mkdir -p "$MOUNT_DIR"
            echo "挂载中... 浏览路径: $MOUNT_DIR"
            $RESTIC_BIN mount "$MOUNT_DIR" -r $REPO --password-file $PASSFILE &
            echo "已挂载,可用 'fusermount -u $MOUNT_DIR' 卸载。"
            pause
            ;;
        10)
            read -p "请输入要卸载的目录(默认 $DEFAULT_MOUNT): " MOUNT_DIR
            MOUNT_DIR=${MOUNT_DIR:-$DEFAULT_MOUNT}
            fusermount -u "$MOUNT_DIR" 2>/dev/null || umount "$MOUNT_DIR"
            echo "已卸载 $MOUNT_DIR"
            pause
            ;;
        11)
            echo "执行自定义 forget 策略示例:"
            echo "保留最近7天, 4周, 12月, 每年1个快照"
            read -p "模拟执行查看将删除哪些快照?(yes/no): " DRYRUN
            if [ "$DRYRUN" == "yes" ]; then
                $RESTIC_BIN forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 1 --prune --dry-run -r $REPO --password-file $PASSFILE | less -SFXR
            fi
            read -p "确认执行实际删除?(yes/no): " CONFIRM
            if [ "$CONFIRM" == "yes" ]; then
                $RESTIC_BIN forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 1 --prune -r $REPO --password-file $PASSFILE
                echo "删除完成。"
            else
                echo "已取消删除。"
            fi
            pause
            ;;
        0)
            echo "退出菜单。"
            exit 0
            ;;
        *)
            echo "无效选择,请重试。"
            pause
            ;;
    esac
done

也可以前往资源库搜索下载



新full-backup.sh脚本

之前的脚本功能比较简单,所以更新了一下脚本


MySQL 数据库备份

支持备份单个数据库、多数据库或全部数据库(排除系统库)。

严格检查备份完整性:

捕获 mysqldump 返回码和错误输出。

能区分错误类型:

用户名或密码错误 → “连接数据库失败(用户名或密码错误)”

数据库不存在 → “数据库不存在”

无法连接 MySQL 主机/端口 → “无法连接数据库(主机或端口错误)”

其他未知错误 → “未知错误(请检查数据库是否存在或权限是否正确)”

如果有任何数据库导出失败:

会将所有失败的数据库和原因汇总在邮件中。

立即停止脚本执行,确保数据完整性。

成功导出数据库会生成 .sql 文件,并显示文件大小(MB/GB)。


文件/目录备份

支持通过 backup_dirs.txt 列表备份任意目录。

严格检查目录存在性:

在开始备份前逐个检查目录是否存在且可访问。

若发现目录不存在或不可访问:

立即停止备份。

发送邮件通知,并列出出错目录。

使用 Restic 进行备份。

捕获 Restic 输出,若失败立即停止并发送详细邮件。

每个目录备份完成后,会统计并记录大小。

总备份大小统计

使用 Restic stats --mode raw-data --last 1 获取最新快照的实际数据大小。

自动将字节转换为 MB 或 GB 显示,确保总备份大小准确。


快照 ID

获取最新 Restic 快照的短 ID 并显示,便于后续恢复或管理。


邮件通知

支持成功和失败邮件通知。

失败邮件:

包含 MySQL 导出失败详情或文件目录备份失败目录。

成功邮件:

列出所有 MySQL 数据库导出文件及大小。

列出所有备份目录及大小。

总备份大小、快照 ID、备份开始和结束时间、耗时。


进度与日志

控制台输出详细进度:

MySQL 每个数据库导出情况。

文件/目录备份列表。

每个文件/目录的大小。

方便用户监控备份状态。


数据完整性保证

严格模式:

任何数据库导出失败或文件目录备份失败,脚本立即停止。

避免不完整备份进入 Restic 仓库。

邮件提供详细失败信息,便于快速定位问题。


脚本内容

#!/bin/bash
# -------------------------------
# 全量备份 MySQL + 文件到 Restic
# -------------------------------
# 作者:贰叁肆博客
# -------------------------------

# ---------------- 配置 ----------------
# MySQL 配置
MYSQL_USER="数据库用户名"
MYSQL_PASSWORD="数据库密码"
MYSQL_HOST="数据库地址"
MYSQL_BACKUP_DIR="导出路径"
DB_TO_BACKUP=数据库名"   # 0表示备份所有数据库,多个用空格隔开

# 邮件配置
EMAIL="收件地址"
MAIL_ENABLE=true  # 邮件通知开关 true/false

# Restic 配置
RESTIC_BIN="restic二进制文件路径"
RESTIC_REPO="备用仓库路径"
RESTIC_PASSWORD_FILE="密码文件路径"
BACKUP_DIRS_FILE="备份列表文件路径"

mkdir -p "$MYSQL_BACKUP_DIR"

# ---------------- 邮件发送函数 ----------------
send_mail() {
    if [ "$MAIL_ENABLE" = true ]; then
        local subject="$1"
        local body="$2"
        echo -e "$body" | mail -s "$subject" -r "服务器备份 <发件邮箱>" "$EMAIL"
    fi
}

# ---------------- 开始计时 ----------------
START_TIME=$(date +%s)
BACKUP_START=$(date '+%Y-%m-%d %H:%M:%S')

# ---------------- 1. MySQL 备份 ----------------
FAILED_DBS=()
DB_SIZE_INFO=""

if [ "$DB_TO_BACKUP" == "0" ]; then
    DBS=$(/usr/bin/mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -h "$MYSQL_HOST" -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|mysql|sys)")
else
    DBS="$DB_TO_BACKUP"
fi

for db in $DBS; do
    echo "==== 开始备份数据库: $db ===="
    echo "正在导出数据库 $db ..."

    # 捕获 mysqldump 输出和返回码
    DUMP_OUTPUT=$(/usr/bin/mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -h "$MYSQL_HOST" "$db" 2>&1)
    EXIT_CODE=$?

    if [ $EXIT_CODE -ne 0 ]; then
        rm -f "$MYSQL_BACKUP_DIR/$db.sql"

        if echo "$DUMP_OUTPUT" | grep -q "Access denied"; then
            FAIL_REASON="连接数据库失败(用户名或密码错误)"
        elif echo "$DUMP_OUTPUT" | grep -q "Unknown database"; then
            FAIL_REASON="数据库不存在"
        elif echo "$DUMP_OUTPUT" | grep -q "Can't connect to MySQL server" || \
             echo "$DUMP_OUTPUT" | grep -q "Lost connection to MySQL server"; then
            FAIL_REASON="无法连接数据库(主机或端口错误)"
        else
            FAIL_REASON="$DUMP_OUTPUT"
        fi

        FAILED_DBS+=("$db : $FAIL_REASON")
        continue
    fi

    echo "$DUMP_OUTPUT" > "$MYSQL_BACKUP_DIR/$db.sql"

    SIZE_BYTES=$(stat -c%s "$MYSQL_BACKUP_DIR/$db.sql" 2>/dev/null)
    SIZE_BYTES=${SIZE_BYTES:-0}
    if [ "$SIZE_BYTES" -ge 1073741824 ]; then
        SIZE_FMT=$(awk "BEGIN{printf \"%.2f GB\", $SIZE_BYTES/1024/1024/1024}")
    else
        SIZE_FMT=$(awk "BEGIN{printf \"%.2f MB\", $SIZE_BYTES/1024/1024}")
    fi
    echo "数据库 $db 导出完成,文件大小: $SIZE_FMT"
    DB_SIZE_INFO+="$db.sql : $SIZE_FMT\n"
done

if [ ${#FAILED_DBS[@]} -ne 0 ]; then
    FAIL_STEP="以下数据库导出失败:\n$(printf "%s\n" "${FAILED_DBS[@]}")"
    send_mail "MySQL 备份失败" "备份失败步骤:\n$FAIL_STEP\n请检查数据库连接或权限。"
    exit 1
fi

echo "==== MySQL 备份完成 ===="

# ---------------- 2. Restic 备份 ----------------
echo "==== 开始 Restic 备份 ===="
echo "备份目录/文件列表:"
cat "$BACKUP_DIRS_FILE"
echo "------------------------"

if ! $RESTIC_BIN -r "$RESTIC_REPO" --password-file "$RESTIC_PASSWORD_FILE" \
    backup --files-from "$BACKUP_DIRS_FILE" --verbose 2>&1 | tee /tmp/restic-backup.log; then
    send_mail "Restic 备份失败" "备份失败步骤: Restic 文件备份失败\n详细信息:\n$(cat /tmp/restic-backup.log)"
    exit 1
fi

echo "==== Restic 备份完成 ===="

# ---------------- 3. 目录大小信息 ----------------
DIR_SIZE_INFO=""
while IFS= read -r line; do
    if [ -e "$line" ]; then
        SIZE_BYTES=$(du -sb "$line" 2>/dev/null | awk '{print $1}')
        SIZE_BYTES=${SIZE_BYTES:-0}
        if [ "$SIZE_BYTES" -ge 1073741824 ]; then
            SIZE_FMT=$(awk "BEGIN{printf \"%.2f GB\", $SIZE_BYTES/1024/1024/1024}")
        else
            SIZE_FMT=$(awk "BEGIN{printf \"%.2f MB\", $SIZE_BYTES/1024/1024}")
        fi
        DIR_SIZE_INFO+="$line : $SIZE_FMT\n"
    else
        DIR_SIZE_INFO+="$line : 不存在\n"
    fi
done < "$BACKUP_DIRS_FILE"

# ---------------- 4. 成功邮件信息 ----------------
END_TIME=$(date +%s)
BACKUP_END=$(date '+%Y-%m-%d %H:%M:%S')
ELAPSED_TIME=$((END_TIME-START_TIME))

# 获取本次备份总大小 (使用 restore-size)
BACKUP_SIZE_BYTES=$($RESTIC_BIN -r "$RESTIC_REPO" --password-file "$RESTIC_PASSWORD_FILE" \
    stats --json --mode restore-size latest | jq -r '.total_size // 0')

if [ "$BACKUP_SIZE_BYTES" -ge 1073741824 ]; then
    BACKUP_SIZE=$(awk "BEGIN{printf \"%.2f GB\", $BACKUP_SIZE_BYTES/1024/1024/1024}")
else
    BACKUP_SIZE=$(awk "BEGIN{printf \"%.2f MB\", $BACKUP_SIZE_BYTES/1024/1024}")
fi

SNAPSHOT_ID=$($RESTIC_BIN -r "$RESTIC_REPO" --password-file "$RESTIC_PASSWORD_FILE" \
    snapshots --json | jq -r 'sort_by(.time)[-1].short_id')

MAIL_BODY="备份成功!

=== MySQL 导出文件 ===
$DB_SIZE_INFO

=== 文件/目录备份 ===
$DIR_SIZE_INFO

总备份大小: $BACKUP_SIZE
快照 ID: $SNAPSHOT_ID
开始时间: $BACKUP_START
结束时间: $BACKUP_END
耗时: ${ELAPSED_TIME}s
"

send_mail "备份成功" "$MAIL_BODY"

echo "==== 备份完成 ===="
echo "总耗时: ${ELAPSED_TIME}s"


二维码

发表评论