有许多工具可以用来跨 UNIX? 目录同步文件,但是要想有效且安全地完成这个任务,就需要多做一些工作。本文介绍跨 UNIX 文件系统和不同的计算机系统安全地同步文件的解决方案,包括如何为了执行备份同步文件的加密版本。
文件同步就是在一个位置添加、修改或删除文件时,在另一个位置添加、修改或删除同一个文件的过程。本文讨论三个实用程序,cp、tar 和 rsync,它们都有助于同步 UNIX 文件。cp 和 tar 命令的同步功能有限,而 rsync 提供很全面的选项;尽管如此,它们都有各自适用的场合。
用 cp 命令执行直接复制
尽管 cp 命令并不是真正的同步命令,但它可能是在两个位置之间复制文件的最简单方法。对于单一文件复制,cp 显然是非常高效的:$ cp source destination。
要想复制整个目录结构,可以使用 -r 选项递归地把整个目录结构从一个位置复制到另一个位置:$ cp -r source destination。这种复制方法仅仅递归地复制文件和目录。文件的权限、所有者和其他元数据并不会复制到目标位置。可以使用 -p 选项保留复制的每个文件和目录的所有者、权限和时间:$ cp -pr source destination。
使用 cp 命令是最容易最公认的文件复制方法,但是 cp 的效率很低,而且如果不使用 NFS 这样的远程文件系统解决方案,就不可能把目录复制到远程系统上。
使用 tar
tar(tape archive 的简写)实用程序原来用于高效地把目录结构(包括文件和文件元数据)转换为二进制流,然后就可以把二进制流写到备份磁带上。
通常使用 tar 创建一个包含所需目录的 .tar 文件:$ tar cf mydir.tar ./mydir。c 选项让 tar 创建新的存档文件,f 选项使用后面的参数指定要创建的存档文件的名称 (mydir.tar)。其余参数指定应该包含在存档文件中的文件或目录。tar 命令自动地递归遍历目录结构,所以如果指定包含一个目录,tar 将在存档文件中包含这个目录以及其中的所有文件和目录。
tar 的一个重要特点是,用户指定的路径名被看作绝对路径。也就是说,如果为 tar 指定完整的目录位置,例如 /etc 目录:$ tar cf etc.tar /etc。那么,在默认情况下,tar 将把文件提取到它们的绝对位置。例如,如果提取这个存档文件:$ tar xf etc.tar,就会在 /etc 目录中重新创建文件和目录结构。这可能会产生破坏(可能会覆盖 /etc 中希望保留的文件)。这个问题有两种解决方法。第一种是使用 GNU tar,它支持通过 –strip-path 选项从提取的路径中删除元素。
另一个简单的解决方法是进入父目录,然后使用相对路径(见清单 1)。
清单 1. 进入父目录并使用相对路径
$ cd /
$ tar cf etc.tar ./etc
在提取存档文件时,会在相对位置重新创建文件。可以使用这种方法帮助同步目录。因为 tar 创建目录结构的字节流,可以通过结合使用 tar 和管道把文件从一个位置复制到另一个位置:$ tar cf – ./etc |( cd /backup; tar xf – )。“-” 指定 tar 应该使用标准输出(在写时)或标准输入(在读时)。圆括号让语句在一个子 shell 中执行。看一下管道符前面的代码,它创建文件的字节流并发送到标准输出。在管道符后面,切换到另一个目录,然后从标准输入提取字节流。
要想保留文件的所有者和权限,可以使用 p 选项保留每个文件和目录的元数据:$ tar cfp – ./etc |( cd /backup; tar xfp – )。
掌握这种基本结构之后,就可以执行更复杂的操作。例如,可以只复制在特定时间之后修改过的文件:$ tar cf – –newer 20090101 ./etc |(cd /backup; tar xf – )。这个命令创建在 2009 年 1 月 1 日之后修改过的文件的拷贝。
通过与 rsh 或 ssh 结合使用,还可以把文件同步到远程主机:$ tar cfp – ./etc |(ssh user@host — tar xfp -)。按照这种方式使用 ssh 和 tar 是在远程主机上创建本地文件备份的好方法。但是,还有更高效的信息同步方法。
使用 rsync 进行智能化同步
前面介绍的文件同步方法的主要问题是,它们会复制每个文件(和相关联的目录结构)。如果您打算创建信息的新拷贝,这就没关系;但是,如果要同步两个目录中的信息,这种方法的效率很低。
假设一个目录中有 10,000 个文件,它们占用 100GB 的空间。如果修改了一个 10MB 的文件,在使用 cp 或 tar 进行同步时,就必须重新复制所有 100GB 文件。对于备份,复制如此大量的信息是很过分的。我们希望尽可能快速有效地完成备份。显然,如果知道哪些文件修改过了,就可以只复制这些文件,但是不总是能够知道这一信息。
tar 的 –newer 选项的作用是有限的,因为必须知道上一次修改的准确时间。rsync 工具能够解决这个问题。它会比较目录结构和各个文件,判断源目录和目标目录之间的差异。在查明哪些文件和目录已经修改了之后,它只把这些文件和目录复制到目标位置。另外,rsync 对各个文件使用相似的算法,只复制文件中修改过的部分。
按照最简单的形式,可以使用 rsync 把一个目录同步到一个新目录,例如:$ rsync -r a b。这会创建新目录 b,其中包含目录 a 中目录结构的拷贝。-r 选项让 rsync 递归遍历目录并复制整个目录结构。但是,如果目标目录已经存在,就会在目标目录 b 中创建一个新目录 a,其中包含文件的拷贝。这会有一些糟糕的副作用。例如,如果要把多个目录复制到备份目录,就会像清单 2 这样做。
清单 2. 把多个目录复制到备份目录
$ mkdir backup
$ rsync dira backup
$ rsync dirb backup
清单 2 创建目录 backup/dira,其中包含原来 dira 目录的拷贝。它还创建目录 backup/dirb,其中包含原来 dirb 目录的拷贝。后面的情况就不一样了:$ rsync dira backup/dira。在第一次使用时,这个脚本的作用符合期望。但是,在第二次使用时,rsync 会在指定的目标目录中创建目标目录,也就是创建 backup/dira/dira 目录。这不仅没有创建我们需要的结构,还造成了内容重复(其中一个版本是没有同步的)。
在使用 rsync 时,可能需要指定另外几个选项。默认的同步并不复制文件元数据,而且像对待普通文件那样对待某些特殊文件(比如链接)。希望使用的主要选项包括:
●–delete —— 从目标目录中删除源目录中不再存在的文件。默认模式仅仅同步文件修改并创建新文件。在默认情况下,如果在源目录中删除了一个文件,就会忽略它,并不在目标目录中相应地删除它。通过使用这个选项,可以创建完全相同的同步。
●–recursive —— 递归地复制目录和文件。
●–times —— 同步每个文件和目录的修改时间和创建时间。
●–owner —— 如果可能的话,保留文件的所有者。
●–group —— 如果可能的话,保留组所有者。
●–links —— 把符号链接复制为符号链接,而不是复制文件数据并解释源链接。
●–perms —— 保留文件权限。
●–hard-links —— 保留硬链接(在目标目录中创建硬链接),而不是复制文件内容。
其中一部分选项只能在两个系统的配置完全相同的情况下使用。例如,只有在源和目标计算机对相同用户使用相同 ID 的情况下,才能保留文件所有者和组所有者设置。
除了本地复制之外,rsync 还可以使用 ssh 执行远程复制。为此,需要在源目录或目标目录前面指定用户名和远程主机。例如,为了把一个目录同步到远程系统 user 上,执行以下命令:$ rsync –recursive dira user@remote:/backup/dirb。如果没有设置无密码 ssh 连接,那么会提示您输入远程密码。如果已经设置了连接,就可以用这种方法执行无人值守的夜间备份。
还可以对源目录使用相同的用户/密码组合,从而从远程源目录复制到本地目录:$ rsync –recursive user@remote:dira dirb。在通过 Internet 复制到远程系统时,还可以使用 –compress 选项在通过网络传输信息之前压缩信息,与原始字节复制相比,这可以大大提高效率。当然,在复制到远程系统时,如果文件包含敏感信息,可能不希望复制原始文件。在这种情况下,就需要使用加密。
加密同步涉及的文件
使用文件同步解决方案的常见原因之一是,为了创建文件的精确备份,以便在出现问题时能够复制或重建目录结构的元素。
rsync 工具非常适合完成这个任务,因为它只复制两个目录之间有差异的文件,效率很高。更有意义的是,因为 rsync 可以同步到远程系统,所以可以使用它自动创建远程备份,不需要把备份文件单独复制到远程系统。
这个过程的一个限制是,创建的拷贝是未加密的。如果要把文件复制到远程系统,而其他人也能够访问这个远程系统,就需要确保其他人无法读取这些文件(即使他们能够接触到这些文件)。
只使用 rsync 是无法加密文件的。也无法使用 rsync 的算法只加密在上一次同步操作之后修改过的文件。
但是,通过在脚本中执行 rsync,就可以用 rsync 的输出创建文件的辅助拷贝,然后对这个拷贝进行加密。
这个脚本的基本原理是创建原目录结构的两个拷贝。第一个拷贝作为参照拷贝,其中包含目录结构的精确副本。这样,当再次同步目录时,就可以像一般情况一样比较源和目标文件并判断出差异。在 rsync 命令中使用 –itemize-changes 选项,rsync 就会创建一个参照列表,其中列出在同步期间每个文件所发生的情况。输出详细说明文件是否已经修改过(或新建),或文件是否已经删除。清单 3 中给出一个示例。
清单 3. rsync 生成的修改记录
.d..t…… t1/a/
*deleting t1/a/3
.d..t…… t1/b/
>f.st…… t1/b/1
>f+++++++++ t1/b/6
以 .d. 开头的行表示新目录或目录修改。*deleting 行表示文件已经从源目录中删除。>f 行表示文件已经修改过或是新建的文件(>f++++++++)。
通过解析这个输出文件,可以判断出源目录和目标参照目录之间的差异。判断出差异之后,可以在第三个目录中创建原文件的加密版本。通过使用修改记录,只加密(或删除)在上一次同步操作之后修改过的文件。不能使用目录的加密版本直接执行同步,因为文件的加密版本总是与源文件不一样。
完整的脚本见清单 4。
清单 4. 完整脚本
#!/usr/bin/perl
use warnings;
use strict;
use File::Basename;
use File::Path;
my $source = shift;
my $dest = shift;
my $encdest = shift;
if (!defined($source) || !defined($dest) || !defined($encdest))
{
print “Error: Not enough arguments!\n”;
print “Usage: $0 source destination encrypteddest\n”;
exit(1);
}
print STDERR “Running rsync between $source and $dest ($encdest)\n”;
system(“rsync –delete –recursive –times -og –links –perms ” .
“–hard-links –itemize-changes $source $dest ” .
“>/tmp/$$.rsynclog 2>&1”);
open(DATA,”/tmp/$$.rsynclog”) or die “Couldn’t open the rsynclog\n”;
my @changedfiles;
my @delfiles;
while(<DATA>)
{
next if (m/sending incremental file list/);
chomp;
last if (length($_) == 0);
my ($changes,$filename) = split;
push @changedfiles,$filename if ($changes =~ m/^>f/);
push @delfiles,$filename if ($changes =~ m/^\*del/);
}
close(DATA);
my $counter = 0;
foreach my $file (@changedfiles)
{
if (-f “$dest/$file”)
{
my $sourcename = encode_filename(“$dest/$file”);
my $destname = encode_filename(“$encdest/$file”);
my $dirname = dirname(“$encdest/$file”);
mkpath($dirname);
system(sprintf(‘cat “%s” |openssl enc -des3 ‘ .
‘-pass file:/var/lib/passphrase -a >”%s”‘,
$sourcename,$destname));
$counter++;
}
}
my $delcounter = 0;
foreach my $file (@delfiles)
{
unlink(“$encdest/$file”);
$delcounter++;
}
print STDERR “Finished (changed: $counter, deleted: $delcounter)\n”;
unlink(“/tmp/$$.rsynclog”);
sub encode_filename
{
my ($filename) = @_;
$filename =~ s/ /\\ /g;
$filename =~ s/’/\\’/g;
$filename =~ s/”/\\”/g;
$filename =~ s/\(/\\(/g;
$filename =~ s/\)/\\)/g;
$filename =~ s/&/\\&/g;
$filename =~ s/#/\\#/g;
return($filename);
}
这个脚本非常简单,很容易使用。在运行脚本时,指定源目录、参照文件的目标目录和文件加密版本的目标目录:$ rsyncrypt source destination destination.enc。
脚本的第一部分在源和目标目录之间执行基本的同步以判断修改(见清单 5)。这个操作生成记录修改的文件(在 /tmp 目录中)。
清单 5. 在源和目标目录之间执行基本的同步
system(“rsync –delete –recursive –times -og –links –perms ” .
“–hard-links –itemize-changes $source $dest ” .
“>/tmp/$$.rsynclog 2>&1”);
接下来,解析修改的列表,生成已经修改和删除的文件的列表(见清单 6)。
清单 6. 解析修改的列表
while(<DATA>)
{
next if (m/sending incremental file list/);
chomp;
last if (length($_) == 0);
my ($changes,$filename) = split;
push @changedfiles,$filename if ($changes =~ m/^>f/);
push @delfiles,$filename if ($changes =~ m/^\*del/);
}
对于每个修改过的文件,读取参照版本并在加密目标目录中创建加密版本(见清单 7)。
清单 7. 创建每个修改过的文件的加密版本
foreach my $file (@changedfiles)
{
if (-f “$dest/$file”)
{
my $sourcename = encode_filename(“$dest/$file”);
my $destname = encode_filename(“$encdest/$file”);
my $dirname = dirname(“$encdest/$file”);
mkpath($dirname);
system(sprintf(‘cat “%s” |openssl enc -des3 ‘ .
‘-pass file:/var/lib/passphrase -a >”%s”‘,
$sourcename,$destname));
$counter++;
}
}
因为要使用 shell 执行实际的加密,文件名必须进行编码。需要对一些特殊字符进行转义,否则 shell 会解释它们。
对于实际的加密,使用 openssl 和一个简单的文本文件(在 /var/lib/passphrase 中),这个文件包含信息编码所用的密码。还可以创建或使用专门生成的密钥来执行此操作或希望使用的其他加密命令。
最后,因为源目录中可能有已经删除的文件,还要把删除的文件从加密目录内容中删除(见清单 8)。
清单 8. 把删除的文件从加密目录内容中删除
foreach my $file (@delfiles)
{
unlink(“$encdest/$file”);
$delcounter++;
}
这个脚本非常有效,惟一的缺点是它需要信息的两个拷贝(参照目录和加密版本),而不只是一个。另外,为了简化这个过程,并没有把权限、所有者和时间戳信息同步到加密版本,但是很容易添加这个特性。因为这个脚本使用 rsync 生成修改的列表,这会显著减少需要加密的文件数量,可以使用相同的优化算法把新的文件加密版本同步到远程主机,从而只传输在上一次同步操作之后修改过的加密文件。
结束语
本文讨论了几种不同的文件同步方法。基本的 cp 命令并不是真正的同步命令,但是可以用来执行直接复制。对于真正的同步操作,cp 命令花费的时间太长,效率很低。在使用 tar 时,可以指定一个时间参照点,只复制在这个时间点之后修改过的文件。但是,如果修改不明显或无法通过简单的比较查明,这个特性的意义也不大。
rsync 工具是更好的文件同步解决方案。它对源和目标目录执行许多检查和比较,可以实现高效的同步,甚至可以通过网络或公共连接执行同步。为了确保安全,可以结合使用 rsync 与加密技术,确保在没有正确的密码或加密密钥的情况下无法读取远程文件。