简介
之前我的博客和服务器上的其他系统都是我手动备份数据的,因为懒,所以懒得整自动备份
最近连手动备份也懒了,想想还是整个自动备份吧
我使用的工具是restic是本地/远程备份工具,可以实现增量备份 + 去重 + 压缩 + 加密 + 快照管理
可以把数据备份到 本地磁盘、NAS、SFTP、S3 等支持的后端
功能 | 描述 |
---|---|
增量备份 | 每次只保存变化的数据,节省空间 |
去重 | 不同备份间重复数据只保存一次 |
压缩 | 数据存储时自动压缩,节省存储空间 |
加密 | 备份自动加密,保证安全 |
快照管理 | 可以查看、保留、删除历史版本 |
跨平台 | Linux / Windows / macOS 都支持 |
思路
我的服务器使用的是联想M73,只有一个硬盘位,所以打算外接一个USB硬盘来存储备份文件
刚开始想使用网盘进行备份的,后面发现很多网盘备份工具都不知此个人版的网盘,只支持企业版,当然也不是没有办法,先把网盘挂载成webdav,然后存进去就可以了
但是我懒,反正家里面有硬盘
第一步:格式化硬盘
首先把USB硬盘插上去,然后使用下面命令查看硬盘信息,可以看到这里已经读出我的硬盘了,是/dev/sdc1
lsblk -f
硬盘格式是ntfs,这个格式对linux并不友好,我需要格式化成ext4格式
首先卸载硬盘,如果没有挂载的话就忽略这一步,如果不知道硬盘有没有挂载就直接执行也没事,没有挂载会有提示
umount /dev/sdc1
然后格式化硬盘,中间可能会弹出提示,输入y回车继续
mkfs.ext4 -L backup /dev/sdc1
等待完成
第二步:挂载硬盘
使用下面命令查看确定硬盘的UUID,记得把UUID复制下来
blkid /dev/sdc1
使用vi编辑下面文件
vi /etc/fstab
在文件最后面添加挂载信息,把下面这一行复制修改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
添加完成后,保存退出
执行立即挂载命令,但是我这提示挂载失败,挂载点不存在
mount -a
这个是因为我的系统里面没有/mnt/usb这个目录,所以挂载不上,我先创建一个/mnt/usb目录
mkdir /mnt/usb
然后再次执行挂载命令,提示不是错误,只是 systemd 在提醒:你修改了 /etc/fstab,但 systemd 还缓存着旧配置。 不过 mount -a 是直接读 /etc/fstab 的,不依赖 systemd,所以已经生效了
mount -a
使用下面命令查看是否挂载成功,看到有挂载点就是成功了
lsblk -f
使用下面命令让 systemd 下次启动时用新的配置
systemctl daemon-reload
第三步:下载release
release可以直接使用系统源yum、apt下载,但是版本比较老,我是直接去github项目地址下载的
项目地址:https://github.com/restic/restic/
首先创建一个文件夹,用来存放release的二进制运行文件
mkdir /opt/release
进入刚刚创建的文件夹
cd /opt/release
进入release项目地址
点击顶部的标签
点击最新的版本号
滑倒下面,找到对应自己服务器操作系统和处理器的版本,右键,复制链接地址
回到SSH终端,使用wget命令下载
wget https://github.com/restic/restic/releases/download/v0.18.1/restic_0.18.1_linux_amd64.bz2
下载完成后解压出来,然后使用ls命令查看解压出来的文件
bunzip2 restic_0.18.1_linux_amd64.bz2
ls
解压出来的文件名太长了,我们重命名一下
mv restic_0.18.1_linux_amd64 restic
修改文件权限,让文件可执行
chmod +x restic
第四步:创建备份文件列表
使用vi编辑器创建一个txt文件,名称为:backup_dirs.txt
vi backup_dirs.txt
然后把需要备份的文件目录写进去,这样后面我们需要增加或者删除备份目录就直接修改这个文件列表即可
/home/wwwroot/www.ersansi.top
/root/Dufs-data
/usr/local/frpc
/usr/local/hysteria2
/root/mysql-bak-up
编辑完成后保存并退出
第五步:初始化备份仓库
我们先在刚刚挂载的USB移动硬盘上创建一个文件夹,用于存放备份文件
mkdir -p /mnt/usb/restic-repo
在创建一个密码文件,用于存储备份加密密码,这个密码很重要,恢复备份的时候也需要这个密码
echo "你的备份密码" > /opt/release/.restic-passwd
修改文件权限
chmod 600 .restic-passwd
初始化仓库,看到下图的结果就是初始化成功了
/opt/release/restic -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd init
第六步:测试备份
使用下面命令测试一下是否可以正常备份,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,说明备份成功了
查看一下快照内容
/opt/release/restic -r /mnt/usb/restic-repo --password-file /opt/release/.restic-passwd snapshots
测试还原
/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
可以看到也是可以正常还原的
第七步:创建脚本
因为我的备份有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 + 文件备份成功完成!"
编辑完成后保存退出,然后授权文件可执行
chmod +x full-backup.sh
执行测试一下是否可用
./full-backup.sh
第七步:创建服务
测试脚本成功了,接下来就是创建system的任务文件了
vi /etc/systemd/system/full-backup.service
把下面内容复制粘贴进去,记得把路径改成自己脚本所在的路径
[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
编辑完成后保存并退出,然后修改权限为可执行权限
第八步:创建定时器
上面创建了服务,用来运行sh脚本,但是我们还需要一个定时器来触发服务,这样就可以实现再指定的时间断进行备份了
vi /etc/systemd/system/full-backup.timer
然后把下面内容复制粘贴进去
[Unit]
Description=每天凌晨 3 点执行全量备份
[Timer]
Unit=full-backup.service
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
OnCalendar=-- 03:00:00 表示每天的 03:00:00 运行(-- 代表任意年-月-日)
Persistent=true 如果机器在 03:00 没开机,开机后会立即补跑一次(防止错过)
编辑好后保存并退出,然后修改权限为可执行
chmod +x /etc/systemd/system/full-backup.timer
第九步:应用配置
所有文件全部配置好后,我们就可以应用配置了
首先重载一下systemd配置文件
systemctl daemon-reload
然后设置定时器开机就启动
systemctl enable full-backup.timer
最后手动启动定时器
systemctl start full-backup.timer
启动完成后我们可以执行下面命令查看定时任务
systemctl list-timers --all
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,需要的可以下载把里面的环境参数修改一下即可使用
下面是菜单脚本的内容,记得把上面的环境变量修改和自己相符的
#!/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"