rsync 在操作同步的时候如果源地址为目录的话是有操作区别的。
具体表现在:
源地址如果是目录的时候,如果是以
/
为结尾的则将会同步源文件夹和目标文件夹内的内容!例如执行
rsync ./test/ user@host:/target/subdir
时将会同步./test
文件夹内的内容至user@host:/target/subdir
内,而如果传递的参数是./test
的话就是同步到user@host:/target/subdir/test
。
基于以上背景在上周一次上传文件时因为携带了 --delete
参数造成了服务器 /root
目录清空的事故,庆幸的是没有什么重要的项目文件在此目录,但是也对服务器环境造成了不可逆的影响。
总结
- 在使用
rsync
命令时注意目录操作相关特性并仔细检查源地址和目标地址 - 在正式执行
rsync
操作前先携带--dry-run
参数进行预执行操作获取将要影响的文件列表 - 非必要不进行
--delete
操作,实在需要用的话使用--delete-after
参数给自己一个后悔的时间
后续
为避免再出错写了一个 rsync
脚本替代原生 rsync
命令,在每次执行前必须先执行携带 --dry-run
参数的命令确认一下将要影响的文件。
代码如下,使用PHP实现:
#!/usr/bin/env php
<?php
# 初始化变量
$work_dir = trim(`pwd`);
$source = "";
$target = "";
$remaining_index = 1;
$debug = in_array('-v', $argv) || in_array('--verbose', $argv);
$to_index = 0;
$remaining_argv = $argv;
$remaining_argv[0] = '';
# 工具函数
$prepare_path = fn($path) => is_dir($path) ? preg_replace("#(.+?)/?$#", "$1/", $path) : $path;
$now = fn() => date('Y-m-d H:i:s');
$log = fn($msg, $level = 'info', $newline = true) => $debug ? print("[" . $now() . "] [$level] $msg" . ($newline ? "\n" : '')) : null;
$output = fn($msg, $newline = true) => $debug ? $log($msg, newline: $newline) : print("$msg" . ($newline ? "\n" : ''));
# 读取源目录或文件和目标位置
if (in_array('to', $argv) || in_array('>', $argv)) {
$to_index = array_search('to', $argv) ?: array_search('>', $argv);
$log("to 索引位置: $to_index");
if ($to_index > 1) {
$source = $argv[$to_index - 1];
$target = $argv[$to_index + 1];
} else {
$output("参数错误,\"to\" 必须在源文件或目录和目标位置之间!");
exit(1);
}
$remaining_argv[$to_index] = '';
$remaining_argv[$to_index + 1] = '';
$remaining_argv[$to_index - 1] = '';
} elseif (str_contains($argv[1] ?? "", '>')) {
$sources = explode('>', $argv[1]);
$source = $sources[0];
$target = $sources[1];
$remaining_argv[1] = '';
} else {
$source = $argv[1] ?? '';
$target = $argv[2] ?? '';
$remaining_argv[1] = '';
$remaining_argv[2] = '';
}
if (!$target || !$source) {
echo `/usr/bin/rsync --help`;
$output("参数错误,请输入源文件或目录和目标位置!");
exit(1);
}
# 源文件检查
if (!realpath($source)) {
$output("Source File/Directory ($source) Not Exist!");
exit(1);
}
# 准备好源目录或文件和目标位置字符
$source = $prepare_path(realpath($source));
# start sync
$log("开始同步 $source 到 $target");
# 准备命令行
$remaining_argv = array_filter($remaining_argv, fn($v) => !!$v &&
!preg_match('#^(-(v|-verbose|i|-itemize-changes|n|-dry-run|r|-recursive))$#', $v)
);
$cmd = "/usr/bin/rsync $source $target --itemize-changes " . implode(' ', $remaining_argv);
$debug && $cmd .= ' --verbose';
is_dir($source) && $cmd .= ' --recursive';
$output("预执行命令:" . $cmd);
$pipe = popen("$cmd --dry-run", 'r');
$dry_run_outputs = [];
while (!feof($pipe)) {
$line = fgets($pipe);
$output($line, false);
if (preg_match("#^(>|\.|cd|\*)#",$line)){
$dry_run_outputs[] = $line;
}
}
pclose($pipe);
if (count(array_filter($dry_run_outputs)) === 0) {
$output("源目录或文件中没有需要同步的内容,退出!");
exit(0);
}
# 询问是否继续执行
$output("注意:此操作是为目录到目录或者文件到文件的同步操作!如:/foo/faa 同步至 /fbb/fcc 则意为两个文件夹中的内容同步!");
$answer = readline("执行以上操作? [y/N] ");
if (strtolower($answer) !== 'y') {
$output("已取消!");
exit(1);
}
$pipe = popen("$cmd", 'r');
while (!feof($pipe)) {
$line = fgets($pipe);
$output($line, false);
}