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

问题描述

场景

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

问题

rsync不会执行双向同步,因此我必须从服务器到客户端以及从客户端到服务器进行同步。对于刚刚通过运行两个rsync操作进行更改的文件,这种方法行之有效,但是在添加删除文件后失败。如果我不将rsync删除选项一起使用,则客户端将永远无法删除文件,因为从服务器到客户端的同步将恢复文件。如果我使用删除选项,则首先运行从服务器到客户端的同步,然后删除客户端已添加的所有新文件,或者首先运行从客户端到服务器的同步,然后删除其他客户端已添加到服务器的所有新文件。>

问题

显然,rsync本身不能解决这种情况,因为它只是为了使一个位置与另一位置同步而被假定。我确实很想写一些代码,但是我只能依靠POSIX shell脚本,这似乎无法实现我的目标。这样甚至可以用rsync完成吗?

解决方法

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

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

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

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

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

每次成功同步后,我们在meta dir中创建一个文件,该文件列出本地dir中的所有文件,我们需要此文件来跟踪两次同步之间添加或删除的文件。如果不存在此类文件,则我们永远不会运行成功的同步。在那种情况下,我们只需同步远程存储中的所有文件,构建一个这样的文件,就可以完成:

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

为什么( cd "$localDir" && find . ) | sed "s/^\.//"?之后需要为$localDir将文件植根于rsync。如果文件$localDir/test.txt存在,则生成的输出文件行必须为/test.txt,并且别无其他。如果没有cdfind命令的绝对路径,它将包含/..abspath../test.txt,而没有sed它将包含./test.txt。为什么显式sort调用?进一步查看。

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

tmpDir=$( mktemp -d )
trap 'rm -rf "$tmpDir"' EXIT

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

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

好的,那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"

通过将定界符设置为空字符串,join会比较整行。通常,输出将包含两个文件中都存在的所有行,但是我们指示join仅连接其中一个文件的输出行,而该行不能与另一个文件的行匹配。添加了仅在第二个文件中存在的行必须来自文件,并且仅在第一个文件中存在的行必须来自已删除的文件。这就是为什么我在上面使用sort作为join的原因,只有在行按sort排序的情况下才能正常工作。

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

rsync -aum --files-from="$newFiles" "$localDir/" "$remoteStorage"

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

接下来,我们将删除操作从远程存储同步到本地,以获取其他客户端执行的所有更改和文件删除,但是我们排除了已在本地删除的文件,否则这些文件将被还原,这是我们不想要的:

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

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

rsync -aum --delete "$localDir/" "$remoteStorage" 

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

请考虑以下情况:客户端A创建了FileX,但尚未同步。客户端B也稍后创建FileX并立即同步。现在,客户端A执行上面的两个同步时,远程存储上的FileX较新,应该替换客户端A上的FileX,但是不会发生。第一次同步显式排除了FileX(它已添加到客户端A),而第二次同步将不会上载,因为远程存储上的版本较新。

要解决此问题,需要从远程到本地的第三次同步而没有任何排除。因此,您还将获得三个同步操作,并且与我上面介绍的三个同步操作相比,我认为上面的同步总是一样快,有时甚至更快,因此我更喜欢上面的那些,但是选择是您自己选择。另外,如果不需要支持这种极端情况,则可以跳过上一个同步操作。然后,该问题将在下一次同步时自动解决。

最后,--delete表示--delete-before--delete-during,具体取决于您的rsync版本。 You may prefer another或明确指定的删除操作。