Linux 自动备份脚本

写了个用于 Linux 备份的脚本。

Bash 脚本

其中几个用于配置的常量:

  • EXCLUDE 所有不备份的绝对路径,以空格分隔,路径中不能包含空格等特殊字符

  • FILE_NAME_PREFIX 备份的文件名前缀

  • FILE_NAME 备份的文件名,默认是文件名前缀+时间

  • TEMP_PATH 临时文件存放路径

  • BACKUP_PATH 备份文件存放路径

  • BACKUP_MAX 保留备份文件的最大数目,超过时将清理旧备份

  • COPY_TO_REMOTE 是否复制到远程路径

  • REMOTE_PATH 需要保存的远程路径,例如使用 Rclone 挂载的路径

backup.shview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/bin/bash

set -o pipefail

EXCLUDE="/boot /dev /lost+found /media /mnt /proc /run /sys /tmp /var/cache /var/tmp"

FILE_NAME_PREFIX="armbian-backup"
FILE_NAME="${FILE_NAME_PREFIX}_$(date +%Y%m%d-%H%M)"

TEMP_PATH="/var/tmp"
BACKUP_PATH="/mnt/sdcard/armbian-backup"
BACKUP_MAX=3
COPY_TO_REMOTE=true
REMOTE_PATH="/mnt/onedrive/armbian-backup"

log() {
echo "$(date +%F-%T)" "[email protected]"
}

[ "$(whoami)" != root ] && log "Please run as root" && exit 1

[ -d $TEMP_PATH ] || rm -f $TEMP_PATH && mkdir -p $TEMP_PATH && chmod 777 $TEMP_PATH
[ -d $BACKUP_PATH ] || rm -f $BACKUP_PATH && mkdir -p $BACKUP_PATH

check_space() {
# KiB
local avail
avail=$(df -k "$1" | sed -n "2p" | awk '{print $4}')
# reserve 512MiB
[ $((avail - $2)) -gt $((1024 * 512)) ]
}

EXCLUDE_OPT=""
for f in $EXCLUDE; do EXCLUDE_OPT="$EXCLUDE_OPT--exclude=$f "; done
# shellcheck disable=SC2086
size=$(du -sh $EXCLUDE_OPT / | awk '{print $1}')
log "Total size is about: $size"
# shellcheck disable=SC2086
size=$(du -sk $EXCLUDE_OPT / | awk '{print $1}')

tmp_path=$TEMP_PATH
check_space $tmp_path "$size" || tmp_path=$BACKUP_PATH
if ! check_space $tmp_path "$size"; then
log "Space not enough:"
df -h $tmp_path
exit 1
fi

TARBALL="$tmp_path/$FILE_NAME.tar"
log "Archiving to: $TARBALL"
# shellcheck disable=SC2086
tar -cpf - $EXCLUDE_OPT / | pv >"$TARBALL"
if [ $? -lt 2 ]; then
log "Successfully saved to:" && ls -lh "$TARBALL"
else
log "Error happened while archieving"
rm -f "$TARBALL"
exit 1
fi

echo
BACKUP="$BACKUP_PATH/$FILE_NAME.tar.xz"
log "Compressing to: $BACKUP"
size=$(du -sk "$TARBALL" | awk '{print $1}')
if dd if="$TARBALL" bs=8M status=none | pv -s "$size"K | xz -T0 >"$BACKUP"; then
log "Successfully saved to:" && ls -lh "$BACKUP"
log "Removing $TARBALL" && rm -f "$TARBALL"
else
rm -f "$TARBALL"
rm -f "$BACKUP"
log "Failed to compress" && exit 1
fi

count=$(find $BACKUP_PATH -mindepth 1 -maxdepth 1 -type f -name "$FILE_NAME_PREFIX*.tar.xz" | wc -l)
for f in "$BACKUP_PATH"/"$FILE_NAME_PREFIX"*.tar.xz; do
[ "$count" -le $BACKUP_MAX ] && break
log "Removing old backup: $f" && rm -f "$f"
((count -= 1))
done

[ $COPY_TO_REMOTE != true ] && exit 0

echo
[ -d $REMOTE_PATH ] || rm -f $REMOTE_PATH && mkdir -p $REMOTE_PATH
size=$(du -sk "$BACKUP" | awk '{print $1}')
if ! check_space $REMOTE_PATH "$size"; then
log "Space not enough:"
df -h $REMOTE_PATH
exit 1
fi

BACKUP_REMOTE="$REMOTE_PATH/$FILE_NAME.tar.xz"
log "Copying to remote: $BACKUP_REMOTE"
if pv "$BACKUP" >"$BACKUP_REMOTE"; then
log "Successfully saved to:" && ls -lh "$BACKUP_REMOTE"
else
log "Error happened while copying"
rm -f "$BACKUP_REMOTE"
exit 1
fi

将脚本放到 /root/bin/ 下并添加执行权限。

总体过程是,先用 tar 打包存放到 TEMP_PATH,再使用 xz 压缩为最终备份文件并存放到 BACKUP_PATH,如果超出最大数则清理旧备份,最后复制到远程目录。

systemd 定时任务

将以下两个文件存放到 /lib/systemd/system/ 下:

backup.serviceview raw
1
2
3
4
5
[Unit]
Description=Run backup

[Service]
ExecStart=/bin/bash -c "/root/bin/backup.sh >>/var/log/backup.log 2>&1 && echo >>/var/log/backup.log"
backup.timerview raw
1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Run backup weekly

[Timer]
OnCalendar=Mon, 03:00
AccuracySec=1h
Persistent=true

[Install]
WantedBy=timers.target

这里设置为每周一 03:00 开始备份,AccuracySec=1h 表示可以有一小时的容错,这是为了减少唤醒 CPU。

运行并使其开机启动:

1
2
3
$ sudo systemctl daemon-reload
$ sudo systemctl start backup.timer
$ sudo systemctl enable backup.timer

systemctl list-timers 可以查看所有定时任务。