是否可以仅使用 rsync 和 POSIX shell 脚本通过中央服务器同步多个客户端?

Mec*_*cki 2 shell rsync posix sh

场景

我有一个文件服务器作为要同步的文件的主存储,我有几个客户端具有主存储的本地副本。每个客户端都可以更改主存储中的文件、添加新文件或删除现有文件。我希望他们通过定期执行同步操作尽可能保持同步,但我在任何地方都可以使用的唯一工具是rsync,我只能在客户端上运行脚本代码,而不能在服务器上运行。

问题

rsync不执行双向同步,所以我必须从服务器同步到客户端以及从客户端到服务器。这对于刚刚通过运行两次rsync操作更改的文件来说是正常的,但是在添加或删除文件时会失败。如果我不使用rsync删除选项,客户端将永远无法删除文件,因为从服务器到客户端的同步会恢复它们。如果我使用删除选项,则从服务器到客户端的同步首先运行并删除客户端添加的所有新文件,或者从客户端到服务器的同步首先运行并删除其他客户端添加到服务器的所有新文件。

问题

显然rsync单独无法处理这种情况,因为它只能使一个位置与另一个位置同步。我当然需要编写一些代码,但我只能依赖 POSIX shell 脚本,这似乎使我无法实现目标。那么它甚至可以完成rsync吗?

Mec*_*cki 6

此场景需要三个同步操作,并了解自上次同步以来本地客户端添加/删除了哪些文件。这种意识是必不可少的,它建立了一个状态,它rsync没有,就像rsync无状态一样;当它运行时,它对以前或将来的操作一无所知。是的,它可以通过一些简单的 POSIX 脚本来完成。

我们将假设设置了三个变量:

  1. metaDir是客户端可以持久存储与同步操作相关的文件的目录;内容本身未同步。

  2. localDir 是要同步的文件的本地副本。

  3. remoteStorage是任何有效的rsync源/目标(可以是挂载的目录或 rsync 协议端点,带或不带 SSH 隧道)。

每次成功同步后,我们在元目录中创建一个文件,列出本地目录中的所有文件,我们需要它来跟踪在两次同步之间添加或删除的文件。如果不存在这样的文件,我们从未运行过成功的同步。在这种情况下,我们只需同步远程存储中的所有文件,构建这样一个文件,我们就完成了:

filesAfterLastSync="$metaDir/files_after_last_sync.txt"

if [ ! -f "$metaDir/files_after_last_sync.txt" ]; then
    rsync -a "$remoteStorage/" "$localDir"
    ( cd "$localDir" && find . ) | sed "s/^\.//" | sort > "$filesAfterLastSync"
    exit 0
fi
Run Code Online (Sandbox Code Playgroud)

为什么( cd "$localDir" && find . ) | sed "s/^\.//"?文件需要植根以$localDirrsync后用。如果文件$localDir/test.txt存在,则生成的输出文件行必须是/test.txt,没有别的。如果没有命令cd的绝对路径find,它将包含/..abspath../test.txt和没有sed它会包含./test.txt. 为什么要显式sort调用?往下看。

如果这不是我们的初始同步,我们应该创建一个临时目录,在脚本终止时自动删除自己,无论哪种方式:

tmpDir=$( mktemp -d )
trap 'rm -rf "$tmpDir"' EXIT
Run Code Online (Sandbox Code Playgroud)

然后我们创建当前在本地目录中的所有文件的文件列表:

filesForThisSync="$tmpDir/files_for_this_sync.txt"
( cd "$localDir" && find . ) | sed "s/^\.//" | sort  > "$filesForThisSync"
Run Code Online (Sandbox Code Playgroud)

现在为什么会有那个sort电话?原因是我需要将文件列表排序在下面。好的,但是为什么不告诉find对列表进行排序呢?那是因为find不能保证排序与sort(在手册页上明确记录的)相同,我需要完全按照sort产生的顺序进行排序。

现在我们需要创建两个特殊文件列表,一个包含自上次同步以来添加的所有文件,另一个包含自上次同步以来删除的所有文件。仅使用 POSIX 这样做有点棘手,但存在各种可能性。这是其中之一:

newFiles="$tmpDir/files_added_since_last_sync.txt"
join -t "" -v 2 "$filesAfterLastSync" "$filesForThisSync" > "$newFiles"

deletedFiles="$tmpDir/files_removed_since_last_sync.txt"
join -t "" -v 1 "$filesAfterLastSync" "$filesForThisSync" > "$deletedFiles"
Run Code Online (Sandbox Code Playgroud)

通过将分隔符设置为空字符串,join比较整行。通常输出将包含两个文件中存在的所有行,但我们指示 join 仅输出一个文件的行,这些行无法与另一个文件的行匹配。仅存在于第二个文件中的行必须来自已添加的文件,仅存在于第一个文件中的行必须来自已删除的文件。这就是为什么我sort在上面使用asjoin只有当行按sort.

最后我们执行三个同步操作。首先,我们将所有新文件同步到远程存储,以确保在开始执行删除操作时不会丢失这些文件:

rsync -aum --files-from="$newFiles" "$localDir/" "$remoteStorage"
Run Code Online (Sandbox Code Playgroud)

什么是-aum-a意味着存档,这意味着同步递归,保留符号链接,保留文件权限,保留所有时间戳,尝试保留所有权和组以及其他一些(这是 的快捷方式-rlptgoD)。-u表示更新,这意味着如果目标文件已存在,则仅在源文件具有较新的最后修改日期时才同步。-m意味着修剪空目录(如果不需要,您可以将其省略)。

接下来,我们通过删除从远程存储同步到本地,以获取其他客户端执行的所有更改和文件删除,但我们排除已在本地删除的文件,否则这些文件会恢复我们不想要的内容:

rsync -aum --delete --exclude-from="$deletedFiles" "$remoteStorage/" "$localDir"

Run Code Online (Sandbox Code Playgroud)

最后,我们通过删除从本地同步到远程存储,以更新本地更改的文件并删除本地删除的文件。

rsync -aum --delete "$localDir/" "$remoteStorage" 
Run Code Online (Sandbox Code Playgroud)

有些人可能认为这太复杂了,只需两个同步就可以完成。首先通过删除将远程同步到本地并排除在本地添加或删除的所有文件(这样我们也只需要生成一个特殊文件,这样就更容易生成了)。然后通过删除将本地同步到远程并排除任何内容。然而这种方法是错误的。它需要第三次同步才能正确。

考虑这种情况:客户端 A 创建了 FileX 但尚未同步。客户端 B 稍后也会创建 FileX 并立即同步。现在,当客户端 A 执行上述两个同步时,远程存储上的 FileX 较新,应该替换客户端 A 上的 FileX,但这不会发生。第一次同步明确排除 FileX;它被添加到客户端 A,因此必须排除在第一次同步时不会被删除(客户端 A 不知道 FileX 也被客户端 B 添加并上传到远程)。而第二个只会上传到远程并排除 FileX,因为远程一个更新。同步后,客户端 A 有一个过时的 FileX,尽管远程上存在更新的 FileX。

为了解决这个问题,需要从远程到本地的第三次同步,没有任何排除。所以你最终也会得到三个同步操作,与我上面介绍的三个同步操作相比,我认为上面的那些总是同样快,有时甚至更快,所以我更喜欢上面的那些,但是,选择是你的。此外,如果您不需要支持该边缘情况,则可以跳过最后一次同步操作。问题将在下次同步时自动解决。

在脚本退出之前,不要忘记为下一次同步更新我们的文件列表:

 ( cd "$localDir" && find . ) | sed "s/^\.//" | sort > "$filesAfterLastSync"
Run Code Online (Sandbox Code Playgroud)

最后,--delete暗示--delete-before--delete-during,这取决于你的版本rsync您可能更喜欢另一个或明确指定的删除操作。

  • 感谢非常有用的脚本!是否可能缺少元数据的关闭重写?`( cd "$localDir" && 查找 . ) | sed "s/^\.//" | sed "s/^\.//" | 排序>“$filesAfterLastSync”` (2认同)