rsync,一款用于文件同步的程序,可以实现本机、局域网和广域网的文件同步。方便、快捷、高效。rsync默认采用的时间同步方法,即只同步时间不同的文件,而且是增量同步,已存在的文件不会再次复制,只会同步那些不存在的文件。首先,我们要进行服务器间的文件同步,必须保证两台服务器所使用的时区相同。我这里使用的测试环境是Debian 9.4 64位,时区是中国上海时区(因为在Linux下没有中国北京时区,上海时区跟北京东八区是一样的)。

timedatectl set-timezone Asia/Shanghai使用上面这个命令即可把时区设置中国时间。然后使用timedatectl即可查看当前系统时间,可以看到当前时间已经是北京时间了

img

下面我们安装rsync程序(Debian系统默认已经安装了)

1
apt install rsync -y

rsync参数有

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
-v, --verbose 详细模式输出
-q, --quiet 精简输出模式
-c, --checksum 打开校验开关,强制对文件传输进行校验
-a, --archive 归档模式,表示以递归方式传输文件,并保持所有文件属性,等于-rlptgoD
-r, --recursive 对子目录以递归模式处理
-R, --relative 使用相对路径信息
-b, --backup 创建备份,也就是对于目的已经存在有同样的文件名时,将老的文件重新命名为~filename。可以使用--suffix选项来指定不同的备份文件前缀。
--backup-dir 将备份文件(如~filename)存放在在目录下。
-suffix=SUFFIX 定义备份文件前缀
-u, --update 仅仅进行更新,也就是跳过所有已经存在于DST,并且文件时间晚于要备份的文件。(不覆盖更新的文件)
-l, --links 保留软链结
-L, --copy-links 像对待常规文件一样处理软链接
--copy-unsafe-links 仅仅拷贝指向SRC路径目录树以外的链接
--safe-links 忽略指向SRC路径目录树以外的链接
-H, --hard-links 保留硬链接
-p, --perms 保持文件权限
-o, --owner 保持文件属主信息
-g, --group 保持文件属组信息
-D, --devices 保持设备文件信息
-t, --times 保持文件时间信息
-S, --sparse 对稀疏文件进行特殊处理以节省DST的空间
-n, --dry-run显示哪些文件将被传输
-W, --whole-file 拷贝文件,不进行增量检测
-x, --one-file-system 不要跨越文件系统边界
-B, --block-size=SIZE 检验算法使用的块尺寸,默认是700字节
-e, --rsh=COMMAND 指定使用rsh、ssh方式进行数据同步
--rsync-path=PATH 指定远程服务器上的rsync命令所在路径信息
-C, --cvs-exclude 使用和CVS一样的方法自动忽略文件,用来排除那些不希望传输的文件
--existing 仅仅更新那些已经存在于DST的文件,而不备份那些新创建的文件
--delete 删除那些DST中SRC没有的文件
--delete-excluded 同样删除接收端那些被该选项指定排除的文件
--delete-after 传输结束以后再删除
--ignore-errors 即使出现IO错误也进行删除
--max-delete=NUM 最多删除NUM个文件
--partial 保留那些因故没有完全传输的文件,以是加快随后的再次传输
--force 强制删除目录,即使不为空
--numeric-ids 不将数字的用户和组ID匹配为用户名和组名
--timeout=TIME IP超时时间,单位为秒
-I, --ignore-times 不跳过那些有同样的时间和长度的文件
--size-only 当决定是否要备份文件时,仅仅察看文件大小而不考虑文件时间
--modify-window=NUM 决定文件是否时间相同时使用的时间戳窗口,默认为0
-T --temp-dir=DIR 在DIR中创建临时文件
--compare-dest=DIR 同样比较DIR中的文件来决定是否需要备份
-P 等同于 --partial
--progress 显示备份过程
-z, --compress 对备份的文件在传输时进行压缩处理
--exclude=PATTERN 指定排除不需要传输的文件模式
--include=PATTERN 指定不排除而需要传输的文件模式
--exclude-from=FILE 排除FILE中指定模式的文件
--include-from=FILE 不排除FILE指定模式匹配的文件
--version 打印版本信息
--address 绑定到特定的地址
--config=FILE 指定其他的配置文件,不使用默认的rsyncd.conf文件
--port=PORT 指定其他的rsync服务端口
--blocking-io 对远程shell使用阻塞IO
-stats 给出某些文件的传输状态
--progress 在传输时显示传输过程
--log-format=formAT 指定日志文件格式
--password-file=FILE 从FILE中得到密码
--bwlimit=KBPS 限制I/O带宽,KBytes per second
-h, --help 显示帮助信息

实际上我们常用的参数就是avzP和delete

1
2
3
4
5
6
7
8
9
10
11
12
-a 参数,相当于-rlptgoD,
-r 是递归
-l 是链接文件,意思是拷贝链接文件;
-p 表示保持文件原有权限;
-t 保持文件原有时间;
-g 保持文件原有用户组;
-o 保持文件原有属主;
-D 相当于块设备文件;
-z 传输时压缩;
-P 显示传输进度;
-v 显示传输时的进度等信息,和-P有点关系
--delete 在目标目录中删除源目录中不存在的文件

rsync最常用的方式:rsync 参数列表 源目录 目标目录

例如:rsync -avzP –delete /home/a/ /home/b。把/home/a目录下的所有文件备份到/home/b下

1
rsync -avzP --delete /home/a /home/b

把/home/下a目录(包括a目录下所有文件)备份到/home/b下

这里要特别注意:源目录包含/和不包含/是完全不同意义的,带了/的表示目录下的所有文件,不带/的表示包含目录本身

如果同步远程服务器的文件,则使用命令:

rsync -avzP --delete -e "ssh -p ssh的端口号" 远程服务器IP:源目录 本地目录

温馨提示:

1、如果远程服务器的ssh用的是默认端口22,则可以不写 -e “ssh -p ssh的端口号”。

2、服务器IP跟源目录之间要用冒号分隔开

3、第一次连接远程服务器时可能会提示“Are you sure you want to continue connecting (yes/no)?”,输入yes确认连接,然后输入远程服务器的ssh密码即可完成文件同步。

上面的操作都是把远程服务器上的文件同步到本机,那么如果是把本机的文件推送到远程服务器呢?其实命令也是一样的,只不过是把源目录和目标目录调换过来而已。如下命令:rsync -avzP --delete 本机目录 远程服务器IP**:**目标目录

同样的,需要输入远程服务器的ssh密码

配合crontab定时任务来执行文件同步。

1、首先,我们先要知道,如果是不使用ssh密码的话,那么我们就要让rsync以daemon的方式运行,同时我们要为rsync的同步操作创建专用的账号密码。另外,我们还可以配置允许哪些IP进行连接我们的rsync服务器,拒绝哪些IP连接,以及允许同步什么目录。

2、我们先来创建rsync的配置文件,我们将rsync相关的文件全部保存在**/etc/rsync目录下,我们先创建/etc/rsync目录,然后创建rsync.conf**配置文件

1
2
3
mkdir /etc/rsync

vim /etc/rsync/rsync.conf

然后在rsync.conf文件写入如下配置:

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
#指定pid文件路径
pid file = /etc/rsync/rsync.pid

#监听端口默认为873,可以修改为其它端口
port = 873

#使用root账号来同步
uid = root

#使用root用户组来同步
gid = root

#以下参数可以配置到各个模块中,以实现不同模块不同的配置。如果配置在这里,则表示所有模块共用这些参数
use chroot = no

#最大并发连接数,0为不限制
max connections = 0

#日志文件保存路径
log file = /etc/rsync/rsync.log

#连接超时时间
timeout = 600

#只读模式,不让客户端上传文件到服务器
read only = no

#只写模式,不让客户端从服务器上下载文件
write only = no

#是否允许列出模块名
list = yes

#忽略一些无关的I/O错误
ignore errors = yes

#保存用户名和密码的文件,该文件的权限必须是600
secrets file = /etc/rsync/rsync.passwd

#指定允许哪些IP连接,多个IP用空格隔开,*表示所有主机
#hosts allow = *

#指定不允许哪些IP连接,多个IP用空格隔开,*表示所有主机
#hosts deny = *


#web是自定义模块名,可以有N个模块,不同模块对应不同path,就可以实现N个目录被同步
[web]
#需要被同步的目录
path = /home/wwwroot/

#允许访问该模块的用户,多个用户用空格或者逗号分隔(用户名和密码保存在secrets file参数指定的文件中,格式为:用户名:密码)
auth users = rsync

然后在/etc/rsync下建立账号密码文件rsync.passwd,接着写入内容,格式是账号:密码

比如我们上面的配置,配的用户名是rsync,假定密码是123654,那么rsync.passwd的内容是rsync:123654接着我们给这个文件授权为600.

1
chmod 600 /etc/rsync/rsync.passwd

操作完上面的步骤后,我们正式启动rsync程序 –config参数指定我们要用的配置文件路径

1
rsync --daemon --config=/etc/rsync/rsync.conf

此时,服务器端的rsync就已经配置完成了。下面我们配置客户端,客户端就非常简单的,第一步就是apt install rysnc -y安装程序,然后创建/etc/rsync目录接着再创建账号密码文件**/etc/rsync/rsync.passwd**

注意:这里不用填写用户名,只要填写密码即可,例如上面我们指定的密码是123654,那么rsync.passwd的内容就是123654

授权为600:chmod 600 /etc/rsync/rsync.passwd

然后运行rsync,例如:rsync -avzP –delete –password-file=/etc/rsync/rsync.passwd rsync@x.x.x.x::web /home

–password-file指定了密码文件,rsync是用户名,x.x.x.x是服务器的IP,web是服务器端的自定义模块名,/home是备份到本地的home目录执行完上面的命令后,正常的话就可以看到已经开始把服务器/home/wwwroot目录下的全部文件慢慢同步到本机的/home目录下了。下面我们把这个命令加到定时任务中。定时任务我们主要用到crontab,编辑任务列表

1
crontab -e

然后把rsync -avzP --delete --password-file=/etc/rsync/rsync.passwd rsync@x.x.x.x::web /home加进去,变成如下格式:

1
*/5 * * * * rsync -avzP --delete --password-file=/etc/rsync/rsync.passwd rsync@x.x.x.x::web /home

这个表示每5分钟进行同步一次,大家可以留意一下后面任务有没有正常间隔5分钟执行。


Rsync 增量备份方案:从 192.168.6.2 同步 /home 到 192.168.6.1

创建备份脚本/usr/local/bin/backup_home.sh examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

# 配置变量
SOURCE="user@192.168.6.2:/home/"
DEST_BASE="/backup/home"
LOG_FILE="/var/log/backup_home.log"
DATE=$(date +%Y%m%d)
PREVIOUS_DATE=$(date -d "yesterday" +%Y%m%d)
PREVIOUS_BACKUP="${DEST_BASE}/daily.${PREVIOUS_DATE}"

# 创建目标目录
mkdir -p "${DEST_BASE}/daily.${DATE}"

# 执行rsync备份
rsync -avz --delete --link-dest="${PREVIOUS_BACKUP}" \
--log-file="${LOG_FILE}" \
"${SOURCE}" "${DEST_BASE}/daily.${DATE}/"

# 保留最近7天的备份
find "${DEST_BASE}" -name "daily.*" -type d -mtime +7 -exec rm -rf {} \;

周期性完整备份 + 每日增量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# 每月1日做完整备份,其他时间做增量
if [ $(date +%d) -eq "01" ]; then
# 完整备份
rsync -avz --delete user@192.168.6.2:/home/ /backup/home/full.$(date +%Y%m%d)/
else
# 增量备份(基于上月完整备份)
LAST_FULL=$(find /backup/home -name "full.*" -type d | sort | tail -n 1)
rsync -avz --delete --link-dest="$LAST_FULL" user@192.168.6.2:/home/ /backup/home/incr.$(date +%Y%m%d)/
fi

# 保留策略
find /backup/home -name "full.*" -type d -mtime +365 -exec rm -rf {} \; # 保留1年完整备份
find /backup/home -name "incr.*" -type d -mtime +30 -exec rm -rf {} \; # 保留30天增量

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
#!/bin/bash

# ------ 配置部分 ------
SSH_PORT="2233" # SSH 端口
REMOTE_PATH="user@104.168.82.223:/mnt/docker-data/vaultwarden" # 远程路径
DEST_BASE="/backup/vaultwarden" # 本地备份目录
LOG_FILE="/var/log/backup_vaultwarden.log" # 日志文件
DATE=$(date +%Y%m%d) # 当前日期
PREVIOUS_DATE=$(date -d "yesterday" +%Y%m%d) # 昨天日期
PREVIOUS_BACKUP="${DEST_BASE}/daily.${PREVIOUS_DATE}" # 昨天的备份路径
TAR_FILE="${DEST_BASE}/vaultwarden_full_${DATE}.tar.gz" # 打包文件名

# ------ 检查远程路径 ------
if [ -z "$REMOTE_PATH" ]; then
echo "ERROR: Remote path is empty!" | tee -a "$LOG_FILE"
exit 1
fi

# ------ 创建目录 ------
mkdir -p "${DEST_BASE}/daily.${DATE}"

# ------ 1. 增量备份(rsync + SSH端口)------
echo "Starting rsync backup on $(date)" >> "$LOG_FILE"
if [ -d "${PREVIOUS_BACKUP}" ]; then
rsync -avz --delete --link-dest="${PREVIOUS_BACKUP}" \
-e "ssh -p $SSH_PORT" \
--log-file="$LOG_FILE" \
"$REMOTE_PATH" "${DEST_BASE}/daily.${DATE}/"
else
rsync -avz --delete \
-e "ssh -p $SSH_PORT" \
--log-file="$LOG_FILE" \
"$REMOTE_PATH" "${DEST_BASE}/daily.${DATE}/"
fi

# ------ 2. 完整打包备份(tar.gz)------
if [ ! -f "$TAR_FILE" ]; then
echo "Creating full backup archive: $TAR_FILE" >> "$LOG_FILE"
tar -czf "$TAR_FILE" -C "${DEST_BASE}/daily.${DATE}" . 2>> "$LOG_FILE"
else
echo "WARNING: $TAR_FILE already exists, skipping compression." >> "$LOG_FILE"
fi

# ------ 清理旧备份(保留7天)------
echo "Cleaning old backups..." >> "$LOG_FILE"
find "$DEST_BASE" -name "daily.*" -type d -mtime +7 -exec rm -rf {} \;
find "$DEST_BASE" -name "vaultwarden_full_*.tar.gz" -mtime +7 -delete

echo "Backup completed at $(date)" >> "$LOG_FILE"

远程备份+加密

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
#!/bin/bash

# ------ 配置部分 ------
SSH_PORT="2233" # SSH 端口
REMOTE_PATH="user@104.168.82.223:/mnt/docker-data/vaultwarden" # 远程路径
DEST_BASE="/backup/vaultwarden" # 本地备份目录
LOG_FILE="/var/log/backup_vaultwarden.log" # 日志文件
DATE=$(date +%Y%m%d) # 当前日期
PREVIOUS_DATE=$(date -d "yesterday" +%Y%m%d) # 昨天日期
PREVIOUS_BACKUP="${DEST_BASE}/daily.${PREVIOUS_DATE}" # 昨天的备份路径
TAR_FILE="${DEST_BASE}/vaultwarden_full_${DATE}.tar.gz" # 打包文件名
GPG_FILE="${DEST_BASE}/vaultwarden_encrypted_${DATE}.tar.gz.gpg" # GPG 加密文件
SSH_KEY="$HOME/.ssh/id_rsa_backup" # 无密码 SSH 私钥
GPG_PASSFILE="$HOME/.backup_password" # GPG 密码文件(chmod 600)

# ------ 检查远程路径 ------
if [ -z "$REMOTE_PATH" ]; then
echo "ERROR: Remote path is empty!" | tee -a "$LOG_FILE"
exit 1
fi

# ------ 1. 检查 SSH 密钥是否存在 ------
if [ ! -f "$SSH_KEY" ]; then
echo "ERROR: SSH key $SSH_KEY not found!" | tee -a "$LOG_FILE"
exit 1
fi

# ------ 2. 检查 GPG 密码文件是否存在 ------
if [ ! -f "$GPG_PASSFILE" ]; then
echo "ERROR: GPG password file $GPG_PASSFILE not found!" | tee -a "$LOG_FILE"
exit 1
fi

# ------ 3. 创建备份目录 ------
mkdir -p "${DEST_BASE}/daily.${DATE}"

# ------ 4. 增量备份(rsync + SSH 免密)------
echo "Starting rsync backup on $(date)" >> "$LOG_FILE"
if [ -d "${PREVIOUS_BACKUP}" ]; then
rsync -avz --delete --link-dest="${PREVIOUS_BACKUP}" \
-e "ssh -i $SSH_KEY -p $SSH_PORT" \
--log-file="$LOG_FILE" \
"$REMOTE_PATH" "${DEST_BASE}/daily.${DATE}/"
else
rsync -avz --delete \
-e "ssh -i $SSH_KEY -p $SSH_PORT" \
--log-file="$LOG_FILE" \
"$REMOTE_PATH" "${DEST_BASE}/daily.${DATE}/"
fi

# ------ 5. 完整打包备份(tar.gz + GPG 加密)------
if [ ! -f "$TAR_FILE" ]; then
echo "Creating full backup archive: $TAR_FILE" >> "$LOG_FILE"
tar -czf - -C "${DEST_BASE}/daily.${DATE}" . 2>> "$LOG_FILE" | \
gpg -c --batch --passphrase-file "$GPG_PASSFILE" --cipher-algo AES256 -o "$GPG_FILE" 2>> "$LOG_FILE"
else
echo "WARNING: $TAR_FILE already exists, skipping compression." >> "$LOG_FILE"
fi

# ------ 6. 清理旧备份(保留7天增量 + 30天加密备份)------
echo "Cleaning old backups..." >> "$LOG_FILE"
find "$DEST_BASE" -name "daily.*" -type d -mtime +7 -exec rm -rf {} \;
find "$DEST_BASE" -name "vaultwarden_full_*.tar.gz" -mtime +7 -delete
find "$DEST_BASE" -name "vaultwarden_encrypted_*.tar.gz.gpg" -mtime +30 -delete

echo "Backup completed at $(date)" >> "$LOG_FILE"

远程备份+加密(不创建增量文件夹,全量备份压缩)

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
#!/bin/bash

# ------ 配置部分 ------
SSH_PORT="2233" # SSH 端口
REMOTE_PATH="/mnt/docker-data/vaultwarden" # 远程服务器上的路径
REMOTE_HOST="root@104.168.82.223" # 远程主机
DEST_BASE="/backup/vaultwarden" # 本地备份目录
LOG_FILE="/var/log/backup_vaultwarden.log" # 日志文件
DATE=$(date +%Y%m%d) # 当前日期
GPG_FILE="${DEST_BASE}/vaultwarden_encrypted_${DATE}.tar.gz.gpg" # GPG 加密文件
SSH_KEY="/home/yuki/.ssh/authorized_keys" # SSH 私钥
GPG_PASSFILE="/backup/.backup_password" # GPG 密码文件(chmod 600)
KEEP_DAYS=30 # 加密备份保留天数

# ------ 检查配置 ------
if [ -z "$REMOTE_PATH" ]; then
echo "ERROR: Remote path is empty!" | tee -a "$LOG_FILE"
exit 1
fi

if [ ! -f "$SSH_KEY" ]; then
echo "ERROR: SSH key $SSH_KEY not found!" | tee -a "$LOG_FILE"
exit 1
fi

if [ ! -f "$GPG_PASSFILE" ]; then
echo "ERROR: GPG password file $GPG_PASSFILE not found!" | tee -a "$LOG_FILE"
exit 1
fi

# ------ 创建备份目录 ------
mkdir -p "$DEST_BASE"

# ------ 通过SSH流式传输+直接加密备份 ------
echo "Starting direct encrypted backup via SSH on $(date)" >> "$LOG_FILE"
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$REMOTE_HOST" \
"tar -czf - -C \"$REMOTE_PATH\" ." 2>> "$LOG_FILE" | \
gpg -c --batch --passphrase-file "$GPG_PASSFILE" \
--cipher-algo AES256 -o "$GPG_FILE" 2>> "$LOG_FILE"

# ------ 清理旧备份 ------
echo "Cleaning backups older than $KEEP_DAYS days..." >> "$LOG_FILE"
find "$DEST_BASE" -name "vaultwarden_encrypted_*.tar.gz.gpg" -mtime +$KEEP_DAYS -delete

echo "Backup completed at $(date)" >> "$LOG_FILE"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 先删除可能存在的旧备份文件(确保不会出现覆盖提示)
#[ -f "$GPG_FILE" ] && rm -f "$GPG_FILE"
if [ -f "$GPG_FILE" ]; then
echo "$GPG_FILE already exists, overwriting..." | tee -a "$LOG_FILE"
rm -f "$GPG_FILE"
fi

# 添加 --yes 参数:强制 GPG 覆盖已存在的文件
echo "Starting direct encrypted backup via SSH on $(date)" >> "$LOG_FILE"
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$REMOTE_HOST" \
"tar -czf - -C \"$REMOTE_PATH\" ." 2>> "$LOG_FILE" | \
gpg --yes -c --batch --passphrase-file "$GPG_PASSFILE" \
--cipher-algo AES256 -o "$GPG_FILE" 2>> "$LOG_FILE"

本地备份

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
#!/bin/bash
# ------ 配置部分 ------
LOCAL_PATH="/mnt/images-data" # 源路径
DEST_BASE="/mnt/images-data-backup" # 本地备份目录
LOG_FILE="/var/log/backup_cvat.log" # 日志文件
DATE=$(date +%Y%m%d) # 当前日期
PREVIOUS_DATE=$(date -d "yesterday" +%Y%m%d) # 昨天日期
LATEST_BACKUP="${DEST_BASE}/daily.${PREVIOUS_DATE}" # 用于增量备份的基础目录
TAR_FILE="${DEST_BASE}/cvat_full_${DATE}.tar.gz" # 打包文件名
TODAYS_BACKUP="${DEST_BASE}/daily.${DATE}" # 今天的备份目录

# ------ 检查路径 ------
[ -z "$LOCAL_PATH" ] && { echo "ERROR: Local path is empty!" | tee -a "$LOG_FILE"; exit 1; }

# ------ 创建备份目录 ------
mkdir -p "$TODAYS_BACKUP"

# ------ 增量备份 ------
echo "Starting incremental backup on $(date)" >> "$LOG_FILE"

if [ -d "$LATEST_BACKUP" ]; then
echo "Using $LATEST_BACKUP as reference for incremental backup" >> "$LOG_FILE"
rsync -av --delete --link-dest="$LATEST_BACKUP" \
--log-file="$LOG_FILE" \
"$LOCAL_PATH/" "$TODAYS_BACKUP/"
else
echo "No previous backup found, performing full initial backup" >> "$LOG_FILE"
rsync -av --delete \
--log-file="$LOG_FILE" \
"$LOCAL_PATH/" "$TODAYS_BACKUP/"
fi

# ------ 5. 完整打包备份------
echo "Creating full backup archive with pigz: $TAR_FILE" >> "$LOG_FILE"
tar -cf - -C "${DEST_BASE}/daily.${DATE}" . 2>> "$LOG_FILE" | \
pigz -9 -p 8 > "$TAR_FILE" 2>> "$LOG_FILE"

# 检查打包是否成功
if [ $? -eq 0 ]; then
echo "Successfully created pigz compressed archive: $TAR_FILE" >> "$LOG_FILE"
else
echo "Error: Failed to create backup archive with pigz" >> "$LOG_FILE"
exit 1
fi

# ------ 6. 清理旧备份(保留7天增量 + 30天备份)------
echo "Cleaning old backups..." >> "$LOG_FILE"
find "$DEST_BASE" -name "daily.*" -type d -mtime +7 -exec rm -rf {} \; 2>> "$LOG_FILE"
find "$DEST_BASE" -name "cvat_full_*.tar.gz" -mtime +30 -delete 2>> "$LOG_FILE"


if [ $? -eq 0 ]; then
echo "Old backups cleanup completed successfully" >> "$LOG_FILE"
else
echo "Warning: Some errors occurred during cleanup" >> "$LOG_FILE"
fi

本地备份

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
#!/bin/bash
# ------ 配置部分 ------
LOCAL_PATH="/mnt/images-data" # 源路径
DEST_BASE="/mnt/images-data-backup" # 本地备份目录
LOG_FILE="/var/log/backup_cvat.log" # 日志文件
DATE=$(date +%Y%m%d) # 当前日期
PREVIOUS_DATE=$(date -d "yesterday" +%Y%m%d) # 昨天时间
LATEST_BACKUP="${DEST_BASE}/daily.${PREVIOUS_DATE}"
TAR_FILE="${DEST_BASE}/cvat_full_${DATE}.tar.gz"
TODAYS_BACKUP="${DEST_BASE}/daily.${DATE}"

# ------ 初始化日志 ------
{
echo "=== Backup started at $(date) ==="
echo "Source: $LOCAL_PATH"
echo "Destination: $DEST_BASE"
} >> "$LOG_FILE"

# ------ 检查路径 ------
[ -d "$LOCAL_PATH" ] || {
echo "ERROR: Source path $LOCAL_PATH not found!" | tee -a "$LOG_FILE"
exit 1
}

# ------ 创建备份目录 ------
mkdir -p "$TODAYS_BACKUP" || {
echo "ERROR: Failed to create backup directory" | tee -a "$LOG_FILE"
exit 1
}

# ------ 增量备份 ------
echo "[$(date +%H:%M:%S)] Starting rsync..." >> "$LOG_FILE"

# 检查文件是否已存在
if [ -f "$TAR_FILE" ]; then
echo "ERROR: Backup file already exists: $TAR_FILE (will not overwrite)" >> "$LOG_FILE"
exit 1
fi

RSYNC_CMD=(rsync -av --delete --checksum --log-file="$LOG_FILE")
[ -d "$LATEST_BACKUP" ] && RSYNC_CMD+=(--link-dest="$LATEST_BACKUP")

if "${RSYNC_CMD[@]}" "$LOCAL_PATH/" "$TODAYS_BACKUP/"; then
echo "[$(date +%H:%M:%S)] Rsync completed successfully" >> "$LOG_FILE"

# 校验备份完整性
echo "[$(date +%H:%M:%S)] Verifying backup..." >> "$LOG_FILE"
diff -rq "$LOCAL_PATH/" "$TODAYS_BACKUP/" >> "$LOG_FILE" 2>&1 || {
echo "WARNING: Backup verification found differences" >> "$LOG_FILE"
}
else
echo "ERROR: Rsync failed with code $?" >> "$LOG_FILE"
exit 1
fi

# ------ 完整打包备份 ------
echo "[$(date +%H:%M:%S)] Creating compressed archive..." >> "$LOG_FILE"

# 使用exclude排除常见临时文件
EXCLUDE_LIST=("*.tmp" "*.swp" "*.bak" "*~")
TAR_EXCLUDE=""
for pattern in "${EXCLUDE_LIST[@]}"; do
TAR_EXCLUDE+="--exclude='$pattern' "
done

if tar ${TAR_EXCLUDE} -cf - -C "$TODAYS_BACKUP" . 2>> "$LOG_FILE" | \
pigz -9 -p 8 > "$TAR_FILE" 2>> "$LOG_FILE"; then

# 验证压缩包完整性
if pigz -t "$TAR_FILE" &>> "$LOG_FILE"; then
echo "[$(date +%H:%M:%S)] Archive verified successfully" >> "$LOG_FILE"
else
echo "ERROR: Archive verification failed" >> "$LOG_FILE"
exit 1
fi
else
echo "ERROR: Archive creation failed" >> "$LOG_FILE"
exit 1
fi

# ------ 清理旧备份 ------
echo "[$(date +%H:%M:%S)] Cleaning old backups..." >> "$LOG_FILE"

# 保留最近7天增量备份
find "$DEST_BASE" -name "daily.*" -type d -mtime +7 -exec rm -rf {} \; 2>> "$LOG_FILE"

# 保留最近30天完整备份
find "$DEST_BASE" -name "cvat_full_*.tar.gz" -mtime +30 -delete 2>> "$LOG_FILE"

echo "[$(date +%H:%M:%S)] Cleanup completed" >> "$LOG_FILE"
echo "=== Backup finished at $(date) ===" >> "$LOG_FILE"

密钥 本地+远程

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/bin/bash
# =============================================
# CVAT 智能双备份脚本 (本地+群晖NAS)
# 版本:2.1
# 功能:增量备份 + 跨系统一致性清理
# =============================================

# ------ 配置区(根据实际修改)------
LOCAL_PATH="/mnt/images-data" # 源数据目录
LOCAL_BACKUP="/mnt/images-data-backup" # 本地备份根目录
REMOTE_USER="rsync_user" # 群晖SSH账号
REMOTE_HOST="192.168.6.216" # 群晖IP地址
REMOTE_PORT="22" # SSH端口
REMOTE_PATH="/volume1/backup/images-data-backup" # 远程备份路径
LOG_FILE="/var/log/backup_cvat.log" # 日志路径
RETENTION_DAYS=7 # 备份保留天数

# ------ 初始化日志 ------
exec 3>&1 4>&2
trap 'exec 2>&4 1>&3' 0 1 2 3
exec 1>>"$LOG_FILE" 2>&1

log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] \\$1"
}

header() {
echo "========================================"
echo " \\$1"
echo "========================================"
}

header "备份任务启动 $(date '+%Y-%m-%d %H:%M:%S')"
log "配置文件校验中..."

# ------ 环境检查 ------
check_env() {
# 检查源目录
[ -d "$LOCAL_PATH" ] || {
log "错误:源目录不存在 [$LOCAL_PATH]"
exit 1
}

# 检查本地备份目录
mkdir -p "$LOCAL_BACKUP" || {
log "错误:无法创建本地备份目录 [$LOCAL_BACKUP]"
exit 1
}

# 检查SSH连接
if ! ssh -p "$REMOTE_PORT" -o BatchMode=yes -o ConnectTimeout=5 "$REMOTE_USER@$REMOTE_HOST" true; then
log "错误:无法连接到远程服务器 [$REMOTE_USER@$REMOTE_HOST]"
exit 1
fi

# 检查远程目录
if ! ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$REMOTE_PATH'"; then
log "错误:远程目录创建失败 [$REMOTE_PATH]"
exit 1
fi
}

# ------ 本地备份函数 ------
local_backup() {
local DATE=$(date +%Y%m%d)
local TODAYS_BACKUP="$LOCAL_BACKUP/daily.$DATE"
local LATEST_BACKUP=$(ls -d "$LOCAL_BACKUP"/daily.* 2>/dev/null | sort | tail -n 1)

log "开始本地备份 -> $TODAYS_BACKUP"

mkdir -p "$TODAYS_BACKUP" || {
log "错误:本地备份目录创建失败"
return 1
}

local RSYNC_CMD=(rsync -av --delete --checksum)
[ -n "$LATEST_BACKUP" ] && RSYNC_CMD+=(--link-dest="$LATEST_BACKUP")

if "${RSYNC_CMD[@]}" "$LOCAL_PATH/" "$TODAYS_BACKUP/"; then
log "本地备份成功 ($(du -sh "$TODAYS_BACKUP" | cut -f1))"
else
log "错误:本地rsync失败 (代码:$?)"
return 1
fi
}

# ------ 远程备份函数 ------
remote_backup() {
local DATE=$(date +%Y%m%d)
local TODAYS_BACKUP="$REMOTE_PATH/daily.$DATE"
local LATEST_BACKUP=$(ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" \
"ls -d '$REMOTE_PATH'/daily.* 2>/dev/null | sort | tail -n 1")

log "开始远程备份 -> $REMOTE_USER@$REMOTE_HOST:$TODAYS_BACKUP"

local RSYNC_CMD=(rsync -avz --delete --checksum -e "ssh -p $REMOTE_PORT")
[ -n "$LATEST_BACKUP" ] && RSYNC_CMD+=(--link-dest="$LATEST_BACKUP")

if "${RSYNC_CMD[@]}" "$LOCAL_PATH/" "$REMOTE_USER@$REMOTE_HOST:$TODAYS_BACKUP/"; then
log "远程备份成功"
else
log "错误:远程rsync失败 (代码:$?)"
return 1
fi
}

# ------ 智能清理函数 ------
clean_backups() {
header "开始智能清理 (保留${RETENTION_DAYS}天)"

# 获取备份列表
local LOCAL_LIST=$(find "$LOCAL_BACKUP" -maxdepth 1 -name 'daily.*' -type d -printf '%f\n' | sort)
local REMOTE_LIST=$(ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" \
"find '$REMOTE_PATH' -maxdepth 1 -name 'daily.*' -type d -printf '%f\n' | sort")

# 计算截止日期
local CUTOFF_DATE=$(date -d "$RETENTION_DAYS days ago" +%Y%m%d)
log "将清理早于 $CUTOFF_DATE 的备份"

# 合并所有备份日期
local ALL_BACKUPS=$(echo -e "$LOCAL_LIST\n$REMOTE_LIST" | sed 's/daily\.//' | sort -u)

# 清理函数
cleanup() {
local DATE=\$1
local DATE_DIR="daily.$DATE"

# 本地清理
if echo "$LOCAL_LIST" | grep -q "$DATE_DIR"; then
log "删除本地备份: $DATE_DIR"
rm -rf "${LOCAL_BACKUP}/${DATE_DIR}" || \
log "警告:本地删除失败 [$DATE_DIR]"
fi

# 远程清理
if echo "$REMOTE_LIST" | grep -q "$DATE_DIR"; then
log "删除远程备份: $DATE_DIR"
ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" \
"rm -rf '${REMOTE_PATH}/${DATE_DIR}'" || \
log "警告:远程删除失败 [$DATE_DIR]"
fi
}

# 遍历处理
while read -r date_dir; do
local backup_date=${date_dir#daily.}

# 验证日期格式
[[ "$backup_date" =~ ^[0-9]{8}$ ]] || continue

# 判断是否过期
if [[ "$backup_date" -lt "$CUTOFF_DATE" ]]; then
cleanup "$backup_date"
else
log "保留备份: daily.$backup_date"
fi
done <<< "$(echo -e "$LOCAL_LIST\n$REMOTE_LIST" | grep 'daily.' | sort -u)"

log "清理完成"
}

# ------ 主执行流程 ------
check_env

header "备份状态概览"
log "本地备份目录: $(ls -d "$LOCAL_BACKUP"/daily.* 2>/dev/null | wc -l) 个"
log "远程备份目录: $(ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" "ls -d '$REMOTE_PATH'/daily.* 2>/dev/null | wc -l) 个"

if local_backup && remote_backup; then
clean_backups
header "备份成功完成 $(date '+%Y-%m-%d %H:%M:%S')"
else
header "备份失败! $(date '+%Y-%m-%d %H:%M:%S')"
exit 1
fi

exit 0

密钥 本地+远程

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/bin/bash
# ------ 配置部分 ------
LOCAL_PATH="/mnt/images-data" # 源路径
DEST_BASE="/mnt/images-data-backup" # 本地备份目录
REMOTE_USER="sync" # 远程NAS用户名
REMOTE_HOST="192.168.6.216" # 远程NAS地址
REMOTE_PORT="22" # 远程端口
REMOTE_PATH="/home/sync/images-data-backup" # 远程备份路径
LOG_FILE="/var/log/backup_cvat.log" # 日志文件

RETENTION_DAYS=7 # 增量备份保留天数
FULL_RETENTION_DAYS=30 # 完整备份保留天数
COMPRESS_LEVEL=3 # 压缩级别(1-9)
MAX_THREADS=$(($(nproc) * 2)) # 最大线程数(CPU核心x2)

# 日期变量
DATE=$(date +%Y%m%d) # 当前日期
PREVIOUS_DATE=$(date -d "yesterday" +%Y%m%d) # 昨天时间
LATEST_BACKUP="daily.${PREVIOUS_DATE}"
TAR_FILE="cvat_full_${DATE}.tar.gz"
TODAYS_BACKUP="daily.${DATE}"

# rsync参数(根据线程数优化)
RSYNC_OPTS="-av --delete --checksum --partial --progress"
[ $MAX_THREADS -gt 1 ] && RSYNC_OPTS="$RSYNC_OPTS --compress --compress-level=$COMPRESS_LEVEL"

# ------ 初始化日志 ------
{
echo "=== Backup started at $(date) ==="
echo "Source: $LOCAL_PATH"
echo "Local Destination: $DEST_BASE"
echo "Remote Destination: $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH"
echo "Performance Settings:"
echo " Retention: $RETENTION_DAYS days (incremental), $FULL_RETENTION_DAYS days (full)"
echo " Compression Level: $COMPRESS_LEVEL"
echo " Max Threads: $MAX_THREADS"
echo "--------------------------------"
} >> "$LOG_FILE"

# ------ 检查路径 ------
[ -d "$LOCAL_PATH" ] || {
echo "ERROR: Source path $LOCAL_PATH not found!" | tee -a "$LOG_FILE"
exit 1
}

# ------ 本地备份 ------
echo "[$(date +%H:%M:%S)] Starting LOCAL backup (Threads: $MAX_THREADS)..." >> "$LOG_FILE"

# 创建本地备份目录
mkdir -p "${DEST_BASE}/${TODAYS_BACKUP}" || {
echo "ERROR: Failed to create local backup directory" | tee -a "$LOG_FILE"
exit 1
}

# 本地增量备份
LOCAL_RSYNC_CMD=(rsync $RSYNC_OPTS --log-file="$LOG_FILE")
[ -d "${DEST_BASE}/${LATEST_BACKUP}" ] && LOCAL_RSYNC_CMD+=(--link-dest="${DEST_BASE}/${LATEST_BACKUP}")

if "${LOCAL_RSYNC_CMD[@]}" "$LOCAL_PATH/" "${DEST_BASE}/${TODAYS_BACKUP}/"; then
echo "[$(date +%H:%M:%S)] Local rsync completed successfully" >> "$LOG_FILE"

# 校验本地备份完整性
echo "[$(date +%H:%M:%S)] Verifying local backup..." >> "$LOG_FILE"
diff -rq "$LOCAL_PATH/" "${DEST_BASE}/${TODAYS_BACKUP}/" >> "$LOG_FILE" 2>&1 || {
echo "WARNING: Local backup verification found differences" >> "$LOG_FILE"
}
else
echo "ERROR: Local rsync failed with code $?" >> "$LOG_FILE"
exit 1
fi

# ------ 远程备份 ------
echo "[$(date +%H:%M:%S)] Starting REMOTE backup to $REMOTE_HOST (Threads: $MAX_THREADS)..." >> "$LOG_FILE"

# 检查远程连接
if ! ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "mkdir -p ${REMOTE_PATH}/${TODAYS_BACKUP}"; then
echo "ERROR: Failed to create remote backup directory" | tee -a "$LOG_FILE"
exit 1
fi

# 远程增量备份
REMOTE_RSYNC_CMD=(rsync $RSYNC_OPTS --log-file="$LOG_FILE" -e "ssh -p $REMOTE_PORT")
# 检查远程是否有昨天的备份
if ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "[ -d ${REMOTE_PATH}/${LATEST_BACKUP} ]"; then
REMOTE_RSYNC_CMD+=(--link-dest="${REMOTE_PATH}/${LATEST_BACKUP}")
fi

if "${REMOTE_RSYNC_CMD[@]}" "$LOCAL_PATH/" ${REMOTE_USER}@${REMOTE_HOST}:"${REMOTE_PATH}/${TODAYS_BACKUP}/"; then
echo "[$(date +%H:%M:%S)] Remote rsync completed successfully" >> "$LOG_FILE"
else
echo "ERROR: Remote rsync failed with code $?" >> "$LOG_FILE"
exit 1
fi

# ------ 本地完整打包备份 ------
echo "[$(date +%H:%M:%S)] Creating LOCAL compressed archive (Level: $COMPRESS_LEVEL)..." >> "$LOG_FILE"

# 使用exclude排除常见临时文件
EXCLUDE_LIST=("*.tmp" "*.swp" "*.bak" "*~")
TAR_EXCLUDE=""
for pattern in "${EXCLUDE_LIST[@]}"; do
TAR_EXCLUDE+="--exclude='$pattern' "
done

if tar ${TAR_EXCLUDE} -cf - -C "${DEST_BASE}/${TODAYS_BACKUP}" . 2>> "$LOG_FILE" | \
pigz -$COMPRESS_LEVEL -p $MAX_THREADS > "${DEST_BASE}/${TAR_FILE}" 2>> "$LOG_FILE"; then

# 验证压缩包完整性
if pigz -t "${DEST_BASE}/${TAR_FILE}" &>> "$LOG_FILE"; then
echo "[$(date +%H:%M:%S)] Local archive verified successfully" >> "$LOG_FILE"
echo "Archive Size: $(du -sh ${DEST_BASE}/${TAR_FILE} | cut -f1)" >> "$LOG_FILE"
else
echo "ERROR: Local archive verification failed" >> "$LOG_FILE"
exit 1
fi
else
echo "ERROR: Local archive creation failed" >> "$LOG_FILE"
exit 1
fi

# ------ 远程完整打包备份 ------
echo "[$(date +%H:%M:%S)] Creating REMOTE compressed archive (Level: $COMPRESS_LEVEL)..." >> "$LOG_FILE"

# 在远程服务器上创建压缩包
if ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "tar ${TAR_EXCLUDE} -cf - -C ${REMOTE_PATH}/${TODAYS_BACKUP} . 2>> ${REMOTE_PATH}/backup.log | \
pigz -$COMPRESS_LEVEL -p $MAX_THREADS > ${REMOTE_PATH}/${TAR_FILE} 2>> ${REMOTE_PATH}/backup.log"; then

# 验证远程压缩包完整性
if ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "pigz -t ${REMOTE_PATH}/${TAR_FILE}"; then
REMOTE_SIZE=$(ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "du -sh ${REMOTE_PATH}/${TAR_FILE} | cut -f1")
echo "[$(date +%H:%M:%S)] Remote archive verified successfully" >> "$LOG_FILE"
echo "Remote Archive Size: $REMOTE_SIZE" >> "$LOG_FILE"
else
echo "ERROR: Remote archive verification failed" >> "$LOG_FILE"
exit 1
fi
else
echo "ERROR: Remote archive creation failed" >> "$LOG_FILE"
exit 1
fi

# ------ 清理旧备份 ------
echo "[$(date +%H:%M:%S)] Cleaning old backups (Retention: $RETENTION_DAYS days incremental, $FULL_RETENTION_DAYS days full)..." >> "$LOG_FILE"

# 本地清理
# 保留最近N天增量备份
find "$DEST_BASE" -name "daily.*" -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \; 2>> "$LOG_FILE"
# 保留最近M天完整备份
find "$DEST_BASE" -name "cvat_full_*.tar.gz" -mtime +$FULL_RETENTION_DAYS -delete 2>> "$LOG_FILE"

# 远程清理 - 先校验文件是否存在两边
echo "[$(date +%H:%M:%S)] Cleaning REMOTE old backups with verification..." >> "$LOG_FILE"

# 获取远程备份列表
REMOTE_BACKUPS=$(ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "find ${REMOTE_PATH} -name 'daily.*' -type d -mtime +$RETENTION_DAYS -printf '%f\n' 2>/dev/null")
REMOTE_TARS=$(ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "find ${REMOTE_PATH} -name 'cvat_full_*.tar.gz' -mtime +$FULL_RETENTION_DAYS -printf '%f\n' 2>/dev/null")

for backup in $REMOTE_BACKUPS; do
# 检查本地是否还有对应的备份
if [ ! -d "${DEST_BASE}/${backup}" ]; then
echo "Deleting remote ${backup} as it's older than $RETENTION_DAYS days and not present locally" >> "$LOG_FILE"
ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "rm -rf ${REMOTE_PATH}/${backup}" 2>> "$LOG_FILE"
else
echo "Keeping remote ${backup} as it still exists locally" >> "$LOG_FILE"
fi
done

for tarfile in $REMOTE_TARS; do
# 检查本地是否还有对应的压缩包
if [ ! -f "${DEST_BASE}/${tarfile}" ]; then
echo "Deleting remote ${tarfile} as it's older than $FULL_RETENTION_DAYS days and not present locally" >> "$LOG_FILE"
ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "rm -f ${REMOTE_PATH}/${tarfile}" 2>> "$LOG_FILE"
else
echo "Keeping remote ${tarfile} as it still exists locally" >> "$LOG_FILE"
fi
done

echo "[$(date +%H:%M:%S)] Cleanup completed" >> "$LOG_FILE"
echo "=== Backup finished at $(date) ===" >> "$LOG_FILE"

密码本地+远程备份

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#!/bin/bash
# ------ 配置部分 ------
LOCAL_PATH="/mnt/images-data" # 源路径
DEST_BASE="/mnt/images-data-backup" # 本地备份目录
REMOTE_USER="sync" # 远程NAS用户名
REMOTE_HOST="192.168.6.216" # 远程NAS地址
REMOTE_PORT="22" # 远程SSH端口(默认22)
REMOTE_PATH="/home/sync/images-data-backup" # 远程备份路径
LOG_FILE="/var/log/backup_cvat.log" # 日志文件
REMOTE_PASS="12345678" # 远程NAS密码

# 性能参数
RETENTION_DAYS=7 # 增量备份保留天数
FULL_RETENTION_DAYS=30 # 完整备份保留天数
COMPRESS_LEVEL=3 # 压缩级别(1-9)
MAX_THREADS=$(($(nproc) * 2)) # 最大线程数(CPU核心x2)

# 安全设置:限制密码变量作用域
export SSHPASS=$REMOTE_PASS

# 日期变量
DATE=$(date +%Y%m%d) # 当前日期
PREVIOUS_DATE=$(date -d "yesterday" +%Y%m%d) # 昨天时间
LATEST_BACKUP="daily.${PREVIOUS_DATE}"
TAR_FILE="cvat_full_${DATE}.tar.gz"
TODAYS_BACKUP="daily.${DATE}"

# rsync参数(根据线程数优化)
RSYNC_OPTS="-av --delete --checksum --partial --progress"
[ $MAX_THREADS -gt 1 ] && RSYNC_OPTS="$RSYNC_OPTS --compress --compress-level=$COMPRESS_LEVEL"

# ------ 初始化日志 ------
{
echo "=== Backup started at $(date) ==="
echo "Source: $LOCAL_PATH"
echo "Local Destination: $DEST_BASE"
echo "Remote Destination: $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH (Port: $REMOTE_PORT)"
echo "Performance Settings:"
echo " Retention: $RETENTION_DAYS days (incremental), $FULL_RETENTION_DAYS days (full)"
echo " Compression Level: $COMPRESS_LEVEL"
echo " Max Threads: $MAX_THREADS"
echo "--------------------------------"
} >> "$LOG_FILE"

# ------ 检查路径 ------
[ -d "$LOCAL_PATH" ] || {
echo "ERROR: Source path $LOCAL_PATH not found!" | tee -a "$LOG_FILE"
exit 1
}

# ------ 本地备份 ------
echo "[$(date +%H:%M:%S)] Starting LOCAL backup (Threads: $MAX_THREADS)..." >> "$LOG_FILE"

# 创建本地备份目录
mkdir -p "${DEST_BASE}/${TODAYS_BACKUP}" || {
echo "ERROR: Failed to create local backup directory" | tee -a "$LOG_FILE"
exit 1
}

# 本地增量备份
LOCAL_RSYNC_CMD=(rsync $RSYNC_OPTS --log-file="$LOG_FILE")
[ -d "${DEST_BASE}/${LATEST_BACKUP}" ] && LOCAL_RSYNC_CMD+=(--link-dest="${DEST_BASE}/${LATEST_BACKUP}")

if "${LOCAL_RSYNC_CMD[@]}" "$LOCAL_PATH/" "${DEST_BASE}/${TODAYS_BACKUP}/"; then
echo "[$(date +%H:%M:%S)] Local rsync completed successfully" >> "$LOG_FILE"

# 校验本地备份完整性
echo "[$(date +%H:%M:%S)] Verifying local backup..." >> "$LOG_FILE"
diff -rq "$LOCAL_PATH/" "${DEST_BASE}/${TODAYS_BACKUP}/" >> "$LOG_FILE" 2>&1 || {
echo "WARNING: Local backup verification found differences" >> "$LOG_FILE"
}
else
echo "ERROR: Local rsync failed with code $?" >> "$LOG_FILE"
exit 1
fi

# ------ 远程备份 ------
echo "[$(date +%H:%M:%S)] Starting REMOTE backup to $REMOTE_HOST:$REMOTE_PORT (Threads: $MAX_THREADS)..." >> "$LOG_FILE"

# 检查远程连接
if ! sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "mkdir -p ${REMOTE_PATH}/${TODAYS_BACKUP}"; then
echo "ERROR: Failed to create remote backup directory" | tee -a "$LOG_FILE"
exit 1
fi

# 远程增量备份
REMOTE_RSYNC_CMD=(rsync $RSYNC_OPTS --log-file="$LOG_FILE")
# 检查远程是否有昨天的备份
if sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "[ -d ${REMOTE_PATH}/${LATEST_BACKUP} ]"; then
REMOTE_RSYNC_CMD+=(--link-dest="${REMOTE_PATH}/${LATEST_BACKUP}")
fi

# 使用sshpass传递密码给rsync(添加端口参数)
REMOTE_RSYNC_CMD+=(--rsh="sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT")
if rsync "${REMOTE_RSYNC_CMD[@]}" "$LOCAL_PATH/" ${REMOTE_USER}@${REMOTE_HOST}:"${REMOTE_PATH}/${TODAYS_BACKUP}/"; then
echo "[$(date +%H:%M:%S)] Remote rsync completed successfully" >> "$LOG_FILE"
else
echo "ERROR: Remote rsync failed with code $?" >> "$LOG_FILE"
exit 1
fi

# ------ 本地完整打包备份 ------
echo "[$(date +%H:%M:%S)] Creating LOCAL compressed archive (Level: $COMPRESS_LEVEL)..." >> "$LOG_FILE"

# 使用exclude排除常见临时文件
EXCLUDE_LIST=("*.tmp" "*.swp" "*.bak" "*~")
TAR_EXCLUDE=""
for pattern in "${EXCLUDE_LIST[@]}"; do
TAR_EXCLUDE+="--exclude='$pattern' "
done

if tar ${TAR_EXCLUDE} -cf - -C "${DEST_BASE}/${TODAYS_BACKUP}" . 2>> "$LOG_FILE" | \
pigz -$COMPRESS_LEVEL -p $MAX_THREADS > "${DEST_BASE}/${TAR_FILE}" 2>> "$LOG_FILE"; then
# 验证压缩包完整性
if pigz -t "${DEST_BASE}/${TAR_FILE}" &>> "$LOG_FILE"; then
echo "[$(date +%H:%M:%S)] Local archive verified successfully" >> "$LOG_FILE"
echo "Archive Size: $(du -sh ${DEST_BASE}/${TAR_FILE} | cut -f1)" >> "$LOG_FILE"
else
echo "ERROR: Local archive verification failed" >> "$LOG_FILE"
exit 1
fi
else
echo "ERROR: Local archive creation failed" >> "$LOG_FILE"
exit 1
fi

# ------ 远程完整打包备份 ------
echo "[$(date +%H:%M:%S)] Creating REMOTE compressed archive (Level: $COMPRESS_LEVEL)..." >> "$LOG_FILE"

# 在远程服务器上创建压缩包
if sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "tar ${TAR_EXCLUDE} -cf - -C ${REMOTE_PATH}/${TODAYS_BACKUP} . 2>> ${REMOTE_PATH}/backup.log | \
pigz -$COMPRESS_LEVEL -p $MAX_THREADS > ${REMOTE_PATH}/${TAR_FILE} 2>> ${REMOTE_PATH}/backup.log"; then

# 验证远程压缩包完整性
if sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "pigz -t ${REMOTE_PATH}/${TAR_FILE}"; then
REMOTE_SIZE=$(sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "du -sh ${REMOTE_PATH}/${TAR_FILE} | cut -f1")
echo "[$(date +%H:%M:%S)] Remote archive verified successfully" >> "$LOG_FILE"
echo "Remote Archive Size: $REMOTE_SIZE" >> "$LOG_FILE"
else
echo "ERROR: Remote archive verification failed" >> "$LOG_FILE"
exit 1
fi
else
echo "ERROR: Remote archive creation failed" >> "$LOG_FILE"
exit 1
fi

# ------ 清理旧备份 ------
echo "[$(date +%H:%M:%S)] Cleaning old backups (Retention: $RETENTION_DAYS days incremental, $FULL_RETENTION_DAYS days full)..." >> "$LOG_FILE"

# 本地清理
# 保留最近N天增量备份
find "$DEST_BASE" -name "daily.*" -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \; 2>> "$LOG_FILE"
# 保留最近M天完整备份
find "$DEST_BASE" -name "cvat_full_*.tar.gz" -mtime +$FULL_RETENTION_DAYS -delete 2>> "$LOG_FILE"

# 远程清理 - 先校验文件是否存在两边
echo "[$(date +%H:%M:%S)] Cleaning REMOTE old backups with verification..." >> "$LOG_FILE"

# 获取远程备份列表
REMOTE_BACKUPS=$(sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "find ${REMOTE_PATH} -name 'daily.*' -type d -mtime +$RETENTION_DAYS -printf '%f\n' 2>/dev/null")
REMOTE_TARS=$(sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "find ${REMOTE_PATH} -name 'cvat_full_*.tar.gz' -mtime +$FULL_RETENTION_DAYS -printf '%f\n' 2>/dev/null")

for backup in $REMOTE_BACKUPS; do
# 检查本地是否还有对应的备份
if [ ! -d "${DEST_BASE}/${backup}" ]; then
echo "Deleting remote ${backup} as it's older than $RETENTION_DAYS days and not present locally" >> "$LOG_FILE"
sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "rm -rf ${REMOTE_PATH}/${backup}" 2>> "$LOG_FILE"
else
echo "Keeping remote ${backup} as it still exists locally" >> "$LOG_FILE"
fi
done

for tarfile in $REMOTE_TARS; do
# 检查本地是否还有对应的压缩包
if [ ! -f "${DEST_BASE}/${tarfile}" ]; then
echo "Deleting remote ${tarfile} as it's older than $FULL_RETENTION_DAYS days and not present locally" >> "$LOG_FILE"
sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "rm -f ${REMOTE_PATH}/${tarfile}" 2>> "$LOG_FILE"
else
echo "Keeping remote ${tarfile} as it still exists locally" >> "$LOG_FILE"
fi
done

# 清除密码变量
unset REMOTE_PASS
unset SSHPASS

echo "[$(date +%H:%M:%S)] Cleanup completed" >> "$LOG_FILE"
echo "=== Backup finished at $(date) ===" >> "$LOG_FILE"

生成无密码 SSH 密钥

1
2
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa_backup
ssh-copy-id -i ~/.ssh/id_rsa_backup -p 2233 user@104.168.82.223

2. 创建 GPG 密码文件

1
2
echo "你的GPG密码" > ~/.backup_password
chmod 600 ~/.backup_password # 仅限当前用户可读

GPG 加密(避免手动输入密码)

方法 1:使用 --passphrase 直接传入密码(不安全)

1
2
3
4
5
# 加密(密码明文写在脚本里,不推荐!)
gpg -c --batch --passphrase "你的密码" -o backup.gpg backup.tar.gz

# 解密
gpg -d --batch --passphrase "你的密码" -o backup.tar.gz backup.gpg

❌ 缺点:密码会暴露在脚本或命令行历史中,容易被窃取。


方法 2:使用 gpg-agent(推荐)

1
2
3
4
5
# 首次运行时缓存密码(需手动输入一次)
echo "你的密码" | gpg --batch --passphrase-fd 0 -c -o backup.gpg backup.tar.gz

# 后续脚本可自动使用缓存的密码(无需手动输入)
gpg --batch -c -o backup.gpg backup.tar.gz

✅ 优点:比硬编码密码更安全,但仍依赖 gpg-agent 会话。


方法 3:使用密钥文件(更安全)

1
2
3
4
5
6
7
8
9
# 将密码存入文件(设置严格权限!)
echo "你的密码" > ~/.backup_password
chmod 600 ~/.backup_password

# 加密时读取密码文件
gpg -c --batch --passphrase-file ~/.backup_password -o backup.gpg backup.tar.gz

# 解密同理
gpg -d --batch --passphrase-file ~/.backup_password -o backup.tar.gz backup.gpg

✅ 优点:密码不直接暴露在脚本中。
⚠ 注意:确保 ~/.backup_password 仅限当前用户可读(chmod 600)。

生成无密码 SSH 密钥(如果还没生成)

1
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa_backup
  • -N "" 表示不设密码(确保脚本能自动运行)。
  • 生成的密钥:
    • 私钥: ~/.ssh/id_rsa_backup(本地使用,绝不能泄露!)
    • 公钥: ~/.ssh/id_rsa_backup.pub(上传到远程服务器)

2. 上传公钥到远程服务器

1
ssh-copy-id -i ~/.ssh/id_rsa_backup -p 2233 user@104.168.82.223
  • 作用:自动把你的公钥(id_rsa_backup.pub)添加到远程服务器的 ~/.ssh/authorized_keys 文件。
  • 执行后,下次 SSH 登录(或 rsync)时,远程服务器会信任你的本地机器,不再要求密码。

3. 测试免密登录

1
ssh -i ~/.ssh/id_rsa_backup -p 2233 user@104.168.82.223
  • 如果直接进入远程服务器终端(不提示输入密码),说明配置成功!
  • 你的 rsync 命令现在也能免密运行了。

设置脚本权限

1
chmod +x /usr/local/bin/backup_home.sh

配置每日定时任务(每天凌晨2点运行)

1
2
crontab -e                            # sudo select-editor 可以设置默认编辑器
0 2 * * * /usr/local/bin/backup_home.sh

增量备份:使用 –link-dest 参数创建硬链接到前一天的备份,只复制变化的文件
保留策略:脚本会自动删除7天前的备份
日志记录:所有操作记录到 /var/log/backup_home.log
安全删除:–delete 参数确保目标删除源已删除的文件

  1. –link-dest 的真正工作原理
    这个方案使用的是”硬链接式增量备份”,不是简单的文件复制。关键点在于:

每次备份都是完整的:每个 daily.日期 目录看起来都包含完整的 /home 数据
实际存储的是差异:通过硬链接,未修改的文件只占用一个物理存储空间
不删除基础数据:7天前的备份被删除时,只有真正独有的文件会被删除
2. 存储空间如何工作
举例说明7天的备份情况:

/backup/home/
├── daily.20231001 (完整备份)
├── daily.20231002 (链接到20231001的未修改文件 + 新修改的文件)
├── …
└── daily.20231007 (链接到前一天的未修改文件 + 新修改的文件)
当删除 daily.20231001 时:

只有 20231001 独有的文件(后来被修改过的)会被真正删除
其他文件仍被后续备份引用,不会删除
3. 为什么这不是您担心的”删除全量只剩增量”
因为:

每个备份目录看起来都是完整的
物理存储上,多个备份共享相同的未修改文件(通过硬链接)
删除旧备份时,系统会自动维护引用计数,只有真正不被任何备份引用的文件才会被删除

恢复数据

要恢复某天的备份,只需复制相应日期的备份目录即可:

1
cp -a /backup/home/daily.20231001/* /home/

crontab

1
2
sudo systemctl restart cron   # Ubuntu/Debian
sudo systemctl restart crond # CentOS/RHEL

在 cron 中,时间调度是通过 5 个时间字段来控制的,格式如下:

1
分钟(0-59) 小时(0-23) 日(1-31) 月(1-12) 星期(0-7, 0和7都代表周日) 命令

你可以通过调整这些字段来精确控制任务的执行时间,比如 每小时、每天、每周、每月、每年,甚至是 特定日期 或 间隔时间。


📌 1. 基本时间设置

(1) 每小时运行一次

1
0 * * * * /path/to/command  # 每小时的第 0 分钟执行(整点)
  • 示例:0 * * * * /usr/local/bin/backup.sh
  • 效果:00:00, 01:00, 02:00, …, 23:00 各运行一次

(2) 每天固定时间运行

1
30 3 * * * /path/to/command  # 每天凌晨 3:30 执行
  • 示例:30 3 * * * /usr/local/bin/cleanup.sh
  • 效果:每天 3:30 运行

(3) 每周固定时间运行

1
0 20 * * 1 /path/to/command  # 每周一 20:00 执行
  • 星期字段

    :

    • 0 或 7 = 周日
    • 1 = 周一
    • 2 = 周二
    • …
    • 6 = 周六
  • 示例:0 20 * * 1 /usr/local/bin/weekly_report.sh

  • 效果:每周一晚上 8 点运行


(4) 每月 1 号运行

1
0 0 1 * * /path/to/command  # 每月 1 号 00:00 执行
  • 示例:0 0 1 * * /usr/local/bin/monthly_backup.sh
  • 效果:每月 1 号凌晨运行

(5) 每年 1 月 1 日运行

1
0 0 1 1 * /path/to/command  # 每年 1 月 1 日 00:00 执行
  • 示例:0 0 1 1 * /usr/local/bin/yearly_reset.sh
  • 效果:每年元旦凌晨运行

📌 2. 高级时间设置

(6) 每隔 N 分钟/小时运行

1
2
*/15 * * * * /path/to/command  # 每 15 分钟运行一次
0 */2 * * * /path/to/command # 每 2 小时运行一次
  • 示例

    :

    • */5 * * * * /usr/local/bin/monitor.sh(每 5 分钟运行)
    • 0 */6 * * * /usr/local/bin/check_logs.sh(每 6 小时运行)

(7) 特定时间范围运行

1
0 9-17 * * 1-5 /path/to/command  # 周一至周五,9:00-17:00 每小时运行
  • 示例:0 9-17 * * 1-5 /usr/local/bin/office_hours_task.sh
  • 效果:工作日 9:00, 10:00, …, 17:00 各运行一次

(8) 特定日期和时间运行

1
30 16 15 * * /path/to/command  # 每月 15 号 16:30 执行
  • 示例:30 16 15 * * /usr/local/bin/payroll.sh
  • 效果:每月 15 号下午 4:30 运行

📌 3. 特殊用法

(9) 多个时间点

1
0 8,12,18 * * * /path/to/command  # 每天 8:00、12:00、18:00 各运行一次
  • 示例:0 8,12,18 * * * /usr/local/bin/notify.sh
  • 效果:每天 3 个固定时间运行

(10) 随机延迟(避免任务堆积)

1
$(($RANDOM \% 10)) * * * * /path/to/command  # 每小时随机 0-9 分钟运行
  • 示例:$(($RANDOM \% 10)) * * * * /usr/local/bin/random_task.sh
  • 效果:每小时随机时间运行,避免服务器负载高峰

📌 4. 如何编辑 Cron 任务?

(1) 查看当前用户的 Cron 任务

1
crontab -l

(2) 编辑 Cron 任务

1
crontab -e  # 会打开默认编辑器(如 vi/nano)
  • 按格式添加任务,保存退出即可生效。

(3) 删除所有 Cron 任务

1
crontab -r

📌 5. 注意事项

  1. 环境变量问题
    Cron 默认不加载 ~/.bashrc 或 /etc/profile,如果脚本依赖 PATH 等变量,建议在脚本开头设置:

    1
    2
    #!/bin/bash
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
  2. 日志记录
    建议在脚本中记录运行状态:

    1
    echo "$(date): Task started" >> /var/log/cron.log
  3. 权限问题
    确保脚本有执行权限:

    1
    chmod +x /path/to/script.sh

🎯 总结

需求 Cron 表达式 示例
每小时 0 * * * * 0 * * * * /usr/local/bin/hourly_task.sh
每天 30 3 * * * 30 3 * * * /usr/local/bin/daily_backup.sh
每周 0 20 * * 1 0 20 * * 1 /usr/local/bin/weekly_report.sh
每月 0 0 1 * * 0 0 1 * * /usr/local/bin/monthly_cleanup.sh
每年 0 0 1 1 * 0 0 1 1 * /usr/local/bin/yearly_reset.sh
每 15 分钟 */15 * * * * */15 * * * * /usr/local/bin/monitor.sh
工作日 9-17 点每小时 0 9-17 * * 1-5 0 9-17 * * 1-5 /usr/local/bin/office_task.sh
每月 15 号 16:30 30 16 15 * * 30 16 15 * * /usr/local/bin/payroll.sh

如果文件数量很多(几万甚至更多),使用 rsync 默认的单线程传输可能会比较慢。可以采用 多线程加速 的方法来提高传输效率:


方案 1:使用 rsync + parallel 多线程传输

parallel 是一个强大的并行工具,可以配合 rsync 实现多线程传输。

安装 parallel(如果未安装)

1
2
sudo apt install parallel  # Debian/Ubuntu
sudo yum install parallel # CentOS/RHEL

多线程同步命令

1
2
cd /home/yukifuhane/Pictures/pixiv
find . -type f | parallel -j 8 rsync -azP {} root@192.168.6.3:/mnt/docker-data/alist/local-storage/pixiv/{}

参数说明

  • -j 8:使用 8 个并行任务(可根据 CPU 和网络调整)
  • {}:parallel 会自动替换为文件名
  • rsync -azP:保持权限、压缩传输、显示进度

优化

  • 如果文件很大,可以改用 -J(按行分割任务):
    1
    find . -type f | parallel -j 8 -J 100 rsync -azP {} root@192.168.6.3:/mnt/docker-data/alist/local-storage/pixiv/{}
  • 如果文件名包含特殊字符(空格、中文等),改用 -0(NULL 分隔):
    1
    find . -type f -print0 | parallel -0 -j 8 rsync -azP {} root@192.168.6.3:/mnt/docker-data/alist/local-storage/pixiv/{}

方案 2:使用 fpart + rsync 分块并行

fpart 可以按文件大小或数量分块,再并行传输。

安装 fpart

1
2
sudo apt install fpart  # Debian/Ubuntu
sudo yum install fpart # CentOS/RHEL

分块并行传输

1
2
3
4
5
6
7
8
9
cd /home/yukifuhane/Pictures/pixiv
# 1. 生成文件列表
find . -type f > filelist.txt

# 2. 分块(每块最多 1000 个文件)
fpart -f 1000 -o chunks filelist.txt

# 3. 并行传输(8线程)
find chunks.* | parallel -j 8 rsync -azP --files-from={} . root@192.168.6.3:/mnt/docker-data/alist/local-storage/pixiv/

方案 3:使用 rsync + --bwlimit 限速(避免带宽占满)

如果网络带宽有限,可以限制 rsync 的传输速度:

1
rsync -avzP --bwlimit=10M /home/yukifuhane/Pictures/pixiv root@192.168.6.3:/mnt/docker-data/alist/local-storage/pixiv
  • --bwlimit=10M:限制最大带宽为 10MB/s(可调整)

方案 4:使用 lftp(支持多线程 FTP/SFTP)

如果 rsync 仍然较慢,可以改用 lftp,它原生支持多线程传输:

1
lftp -e "mirror -P 8 -R /home/yukifuhane/Pictures/pixiv /mnt/docker-data/alist/local-storage/pixiv; quit" sftp://root@192.168.6.3
  • -P 8:使用 8 个线程
  • -R:递归上传(mirror 默认是下载,-R 改为上传)

总结

方法 适用场景 优点 缺点
rsync + parallel 大量小文件 灵活控制线程数 文件名需处理
fpart + rsync 超大文件集 自动分块 依赖 fpart
rsync --bwlimit 限速传输 防止带宽占满 单线程
lftp SFTP/FTP 传输 原生多线程 非增量同步

如果你的文件数量极多(>10万),推荐 fpart + rsync 或 lftp。
如果是普通几万文件,rsync + parallel 足够高效。