当文件已经在两面时,有什么方法可以同步目录结构?


24

我有两个具有相同文件的驱动器,但是目录结构完全不同。

有什么办法可以“移动”目标端的所有文件,使其与源端的结构相匹配?也许有脚本?

例如,驱动器A具有:

/foo/bar/123.txt
/foo/bar/234.txt
/foo/bar/dir/567.txt

而驱动器B具有:

/some/other/path/123.txt
/bar/doo2/wow/234.txt
/bar/doo/567.txt

有问题的文件很大(800GB),所以我不想重新复制它们;我只想通过创建必要的目录并移动文件来同步结构。

我在考虑一个递归脚本,该脚本将在目标位置找到每个源文件,然后将其移动到匹配的目录,并在必要时创建它。但是-那超出了我的能力!

这里给出了另一个优雅的解决方案:https : //superuser.com/questions/237387/any-way-to-sync-directory-structure-when-the-files-are-already-on-both-sides/238086


您确定名称唯一地确定文件的内容吗,否则您应该考虑按文件的校验和比较文件。
kasterma 2011年

Answers:


11

我将与Gilles一起去,并按照hasen j的建议将您指向Unison 。Unison比DropBox早20年成为DropBox。许多人(包括我自己)每天使用的坚如磐石的代码-非常值得学习。尽管如此,仍join需要获得所有宣传:)


这只是答案的一半,但我必须重新开始工作:)

基本上,我想演示一个鲜为人知的join实用程序,它只是这样做:在某个字段上联接两个表。

首先,设置一个测试用例,包括带空格的文件名:

for d in a b 'c c'; do mkdir -p "old/$d"; echo $RANDOM > "old/${d}/${d}.txt"; done
cp -r old new

(在中编辑一些目录和/或文件名new)。

现在,我们要构建一个映射:每个目录的hash-> filename,然后使用它们join来匹配具有相同哈希值的文件。要生成地图,请将以下内容放入makemap.sh

find "$1" -type f -exec md5 -r "{}" \; \
  | sed "s/\([a-z0-9]*\) ${1}\/\(.*\)/\1 \"\2\"/" \

makemap.sh 吐出一个带有'hash“ filename”'格式的行的文件,因此我们只加入第一列:

join <(./makemap.sh 'old') <(./makemap.sh 'new') >moves.txt

生成moves.txt如下所示:

49787681dd7fcc685372784915855431 "a/a.txt" "bar/a.txt"
bfdaa3e91029d31610739d552ede0c26 "c c/c c.txt" "c c/c c.txt"

下一步将是进行实际的动作,但我的努力就死在报价... mv -imkdir -p应该来得心应手。


对不起,我什么都不懂!

1
join真的很有趣。感谢您引起我的注意。
史蒂文·D

@担。抱歉。问题是我不知道可以对您的文件名做出什么假设。没有假定条件的脚本编写就没有乐趣,尤其是在这种情况下,我选择将文件名输出到文件dwheeler.com/essays/fixing-unix-linux-filenames.html
Janus

1
这可能会浪费大量时间(和CPU负载),因为必须完全读取这些大文件才能创建MD5哈希。如果文件名和文件大小匹配,则对文件进行散列可能是过大的杀伤力。散列应该在第二步中进行,仅适用于名称或大小至少与一个文件(在同一磁盘上)匹配的文件。
Hauke Laging

您不需要对用作join输入的文件进行排序吗?
cjm 2013年

8

有一个称为统一的实用程序:

http://www.cis.upenn.edu/~bcpierce/unison/

网站说明:

Unison是用于Unix和Windows的文件同步工具。它允许将文件和目录集合的两个副本存储在不同的主机(或同一主机上的不同磁盘)上,分别进行修改,然后通过将每个副本中的更改传播到另一个副本来使其更新。

请注意,如果至少一个根是远程的,则Unison仅在第一次运行时检测到移动的文件,因此,即使您正在同步本地文件,也要ssh://localhost/path/to/dir用作根之一。


@吉尔斯:确定吗?我对所有内容都使用统一,并且经常看到它发现已重命名和/或移到很远的文件。您是在说这仅适用于已经同步的文件,其中一致已经有机会记录索引节点号(或它使用的其他任何技巧)?
Janus

@贾努斯:感谢您的更正,我的评论的确是错误的。即使在初次运行时,Unison也会检测到已移动的文件。(当两个根都是本地的时,它不会执行此操作,这就是为什么它在我的测试中未执行此操作。)因此,统一是一个很好的建议。
吉尔(Gilles)'所以

@吉尔斯。提提您,该算法似乎在很多地方区分了本地同步和远程同步。实际上,我认为第一次同步是行不通的。+1一致!
Janus

4

按照hasen j的建议使用Unison。我将这个答案留作可能有用的脚本示例,或仅安装了基本实用程序的服务器上使用。


我假设文件名在整个层次结构中都是唯一的。我还将假设没有文件名包含换行符,并且目录树仅包含目录和常规文件。

  1. 首先在源端收集文件名。

    (cd /A && find . \! -type d) >A.find
  2. 然后将文件移到目标位置。首先,在目标端创建一个扁平化的文件树。如果您想保留旧层次结构中的硬链接,请使用ln代替mv

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. 如果目标中可能缺少某些文件,请创建一个类似的拼合并/A.staging使用rsync将数据从源复制到目标。

    rsync -au /A.staging/ /B.staging/
  4. 现在将文件重命名到位。

    cd /B.new &&
    <A.find perl -l -ne '
      my $dir = '.'; s!^\./+!!;
      while (s!^([^/]+)/+!!) {  # Create directories as needed
        $dir .= "/$1";
        -d $dir or mkdir $dir or die "mkdir $dir: $!"
      }
      rename "/B.staging/$_", "$dir/$_" or die "rename -> $dir/$_: $!"
    '

    等效地:

    cd /B.new &&
    <A.find python -c '
    import os, sys
    for path in sys.stdin.read().splitlines():
        dir, base = path.rsplit("/", 2)
        os.rename(os.path.join("/B.new", base), path)
    '
  5. 最后,如果您关心目录的元数据,请使用已经存在的文件调用rsync。

    rsync -au /A/ /B.new/

请注意,我还没有测试本文中的片段。使用风险自负。请在评论中报告任何错误。


2

特别是如果正在进行的同步很有用,您可以尝试找出 git-annex

它是相对较新的。我没有尝试自己使用它。

我可以提出建议,因为它避免了保留文件的第二个副本……这意味着它必须将文件标记为只读(“锁定”),就像某些非Git版本控制系统一样。

文件由sha256sum +文件扩展名标识(默认情况下)。因此,它应该能够同步两个具有相同文件内容但文件名不同的存储库,而不必执行写入操作(如果需要,还可以通过低带宽网络)。当然,它必须读取所有文件以对它们进行校验和。


1

这样的事情怎么样:

src=/mnt/driveA
dst=/mnt/driveB

cd $src
find . -name <PATTERN> -type f >/tmp/srclist
cd $dst
find . -name <PATTERN> -type f >/tmp/dstlist

cat /tmp/srclist | while read srcpath; do
    name=`basename "$srcpath"`
    srcdir=`dirname "$srcpath"`
    dstpath=`grep "/${name}\$" /tmp/dstlist`

    mkdir -p "$srcdir"
    cd "$srcdir" && ln -s "$dstpath" "$name"
done

这假设您要同步的文件名在整个驱动器中是唯一的:否则,它不可能完全自动化(但是,如果有多个文件,您可以提示用户选择要选择的文件。)

上面的脚本在简单的情况下可以使用,但是如果name碰巧包含对正则表达式具有特殊含义的符号,则可能会失败。在grep对文件列表也可以采取大量的时间,如果有大量的文件。您可以考虑翻译此代码以使用哈希表,该哈希表会将文件名映射到路径,例如在Ruby中。


这看起来很有希望-但是它会移动文件还是只是创建符号链接?

我想我大部分都明白。但是这条grep线是做什么的?它只是找到匹配文件的完整路径dstlist吗?

@Dan:显然是通过使用ln它来创建符号链接。您可以雇用mv移动文件,但要注意覆盖现有文件。另外,在移走文件后,您可能需要清理空目录。是的,该grep命令搜索以文件名结尾的行,从而在目标驱动器上显示其完整路径。
Alex

1

假设基本文件名在树中是唯一的,这很简单:

join <(cd A; find . -type f | while read f; do echo $(basename $f) $(dirname $f); done | sort) \
     <(cd B; find . -type f | while read f; do echo $(basename $f) $(dirname $f); done | sort) |\
while read name to from
do
        mkdir -p B/$to
        mv -v B/$from/$name B/$to/
done

如果要清理旧的空目录,请使用:

find B -depth -type d -delete

1

我也面临这个问题。基于md5sum的解决方案不适用于我,因为我将文件同步到了webdav挂载。计算md5sum和webdav目标也将意味着大文件操作。

我做了一个小脚本reorg_Remote_Dir_detect_moves.sh (在github上),该脚本试图检测移动最多的文件,然后使用几个命令来调整远程目录,以创建一个新的临时shell脚本。由于我只照顾文件名,因此该脚本不是完美的解决方案。

为了安全起见,将忽略几个文件:A)两端都具有相同(相同的开始)名称的文件,以及B)仅位于远程端的文件。它们将被忽略和跳过。

跳过的文件将由您首选的同步工具处理(例如 rsync, unison ...)处理,您需要在运行临时shell脚本后使用该工具。

所以也许我的脚本对某人有用吗?如果是这样(更清楚地说),则分三个步骤:

  1. 运行shell脚本 reorg_Remote_Dir_detect_moves.sh (在github上)
  2. 这将创建临时的shell脚本/dev/shm/REORGRemoteMoveScript.sh=>运行该脚本以进行移动(在mount上会很快webdav
  3. 运行您喜欢的同步工具(例如rsync, unison,...)

1

这是我的答案。作为一个警告,我所有的脚本编写经验都来自bash,因此,如果您使用其他Shell,则命令名称或语法可能会有所不同。

此解决方案需要创建两个单独的脚本。

第一个脚本负责实际在目标驱动器上移动文件。

md5_map_file="<absolute-path-to-a-temporary-file>"

# Given a single line from the md5 map file, list
# only the path from that line.
get_file()
{
  echo $2
}

# Given an md5, list the filename from the md5 map file
get_file_from_md5()
{
  # Grab the line from the md5 map file that has the
  # md5 sum passed in and call get_file() with that line.
  get_file `cat $md5_map_file | grep $1`
}

file=$1

# Compute the md5
sum=`md5sum $file`

# Get the new path for the file
new_file=`get_file_from_md5 $sum`

# Make sure the destination directory exists
mkdir -p `dirname $new_file`
# Move the file, prompting if the move would cause an overwrite
mv -i $file $new_file

第二个脚本创建第一个脚本使用的md5映射文件,然后在目标驱动器中的每个文件上调用第一个脚本。

# Do not put trailing /
src="<absolute-path-to-source-drive>"
dst="<absolute-path-to-destination-drive>"
script_path="<absolute-path-to-the-first-script>"
md5_map_file="<same-absolute-path-from-first-script>"


# This command searches through the source drive
# looking for files.  For every file it finds,
# it computes the md5sum and writes the md5 sum and
# the path to the found filename to the filename stored
# in $md5_map_file.
# The end result is a file listing the md5 of every file
# on the source drive
cd $src
find . -type f -exec md5sum "{}" \; > $md5_map_file

# This command searches the destination drive for files and calls the first
# script for every file it finds.
cd $dst
find . -type f -exec $script_path '{}' \; 

基本上,这是两个脚本用 $md5_map_file。首先,计算并存储源驱动器上文件的所有md5。与md5关联的是从驱动器根目录开始的相对路径。然后,对于目标驱动器上的每个文件,计算md5。使用此md5,在​​源驱动器上查找该文件的路径。然后将目标驱动器上的文件移动到与源驱动器上的文件路径匹配的位置。

此脚本有两个警告:

  • 假定$ dst中的每个文件也位于$ src中
  • 它不会从$ dst中删除任何目录,只会移动文件。我目前无法想到自动执行此操作的安全方法

计算md5的时间将花费很长时间:必须实际读取所有内容。如果Dan确信文件是相同的,则只需将它们移动到目录结构中就非常快(不读取)。因此,md5sum似乎不是在这里使用的东西。(顺便说一句,它rsync有一种不计算校验和的模式。)
imz – Ivan Zakharyaschev 2011年

这是准确性和速度之间的权衡。我想提供一种比简单使用文件名的准确性更高的方法。
cledoux 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.