Linux 简易目录同步工具实现
Linux 简易目录同步工具实现
快速实现功能类似 WinSCP 的同步工具.
基本原理是:
- 用
inotifywait
命令监控目录的变化; - 用
rsync
实现增量同步;
inotifywait
是监控涉及文件的操作的事件的, 我们利用此命令监控需要同步的目录,
有增/删/改等操作时, 命令就返回.
rsync
基本用法见其 man 文档, 本处使用的是 rsync [OPTION...] SRC... [USER@]HOST:DEST
.
实现要求
- 双方都是 Linux 系统.(实际发送方有
inotifywait
与rsync
工具, 且接收方有 ssh 服务器 的应该都行, 但未测试) - 为了能让
rsync
在同步时不用密码, 需要设置无密码ssh
登录.
无密码登录设置
创建 pub key
test -e ~/.ssh/id_rsa.pub || ssh-keygen
命令说明:
- 提示输入密码时, 直接回车, 留空不要输入
- 实际只需执行
ssh-keygen
命令即可, 但有些系统可能已经创建过 pub key, 故命令先测试需要的文件是否存在.
提交 pub key 到远程服务器
# ssh-copy-id <远程服务器登录账号>@<远程地址>, 如:
ssh-copy-id root@10.0.2.2
此处需输入远程服务器上, 对应 <远程服务器登录账号> 的登录密码.
实现
最基本的实现
监听当前目录下的 project
目录, 目标为到 root@localhost:~/ 下
#!/bin/bash
# sync_project.sh file
# chen-qx@live.cn
ret_code=0
do
if [[ $ret_code -eq 0 ]]; then
rsync -e 'ssh -p 20052' -lurpEAXogDth --progress project root@localhost:~/
else
echo "invalid code return from inotifywait $?" >&2
fi
inotifywait -r -e modify -e create -e delete -e move project
ret_code=$?
sleep 1
done
script 使用的命令说明:
inotifywait
的-r
选项是监控目录之内所有内容(包括文件及子目录),-e <event>
是指定文件系统事件, 见参考文档[3]. 如果不确定事件对应哪种操作, 可使用inotifywait -r -m /path/to/foo/
命令监控foo
目录, 再进行些文件操作, 如创建,删除等, 以查看产生的事件名称. 示例中列出事件应该是全了.
rsync
的'ssh -p 20052'
是指定端口的选项. (本处是从物理机同步到虚拟机中)
优化实现
- 实际使用中, 本地删除的文件, 目标服务器上也应该删除, 此时应给
rsync
增加--delete
选项. 因多数是同步源码使用, 加此选项的风险用户自负. - 另外, 文件时, 有些目录或文件是不需要的, 如 vim 会产生个
.xxx.swp
文件, 此时用rsync
的--exclude '.*.swp --exclude '.vscode' --exclude '.git''
来排除. .具体见文档 - 脚本对监控目录及目标目录写死了,不灵活, 使用命令选项比较好.
- ...
最终更改:
#!/bin/bash
# file: sync_project.sh
# chen-qx@live.cn
function usage() {
cat <<EOF
usage:
$0 [-p port] src1 [ src2 src2 ...] dest
EOF
}
declare -a USER_PATHS
SSH_PORT=""
MAYBE_DST=""
while true
do
case $1 in
"-h" | "--help")
usage
exit 0
;;
"-p" | "--port")
shift 1
p=$1
shift 1
tmp_p=$(echo -n $p | sed -r -e 's@^\s*([0-9]+).*@\1@g')
if [[ -n "$tmp_p" && "$tmp_p" != "$p" ]]; then
echo "invalid port: $p" >&2
usage >&2
exit 1
fi
SSH_PORT=$tmp_p
;;
*)
if [[ -z "$1" ]]; then
if [[ -n "$MAYBE_DST" ]]; then
USER_PATHS[${#USER_PATHS[@]}]="$MAYBE_DST"
fi
break
fi
tmp_path="$1"
shift 1
# 应对端口指定在命令末尾的情形
if [[ -n "$MAYBE_DST" ]]; then
echo "invalid source path: $MAYBE_DST" >&2
exit 1
fi
if [[ ! -e "$tmp_path" ]]; then
MAYBE_DST="$tmp_path"
## echo $MAYBE_DST
continue
fi
# duplicate
u_real_path=$(realpath "$tmp_path")
for ((i=0;i<${#USER_PATHS[@]}; i++))
do
real_path=$(realpath "${USER_PATHS[$i]}")
if [[ "$real_path" == "$u_real_path" ]]; then
echo "duplicated path: $tmp_path" >&2
exit 1
fi
done
if [[ "$u_real_path" != "/" ]]; then
# echo "tmp_path: -- $tmp_path"
# rsync 有个特性, 如果源目录以 '/' 结尾, 则目录本身不会同步, 只同步目录下的内容,
# 如有目录结构 a/file1, 则参数写成 a/ 时, 直接将 file1 同步出去, 而 a 目录在目标
# 上并不会被建立.
# 所以, 此处要将路径末尾的 '/' 去掉
tmp_path=$(echo -n "$tmp_path" | sed -r -e 's@(.*[^/])/*$@\1@g')
fi
USER_PATHS[${#USER_PATHS[@]}]="$tmp_path"
;;
esac
done
PATH_COUNT=${#USER_PATHS[@]}
if [[ $PATH_COUNT -lt 2 ]]; then
echo "invalid path" >&2
usage >&2
exit 1
fi
DST_IDX=$(expr $PATH_COUNT - 1 )
DST_PATH="${USER_PATHS[DST_IDX]}"
cat <<EOF
source path: ${USER_PATHS[@]:0:$DST_IDX}
destination path: $DST_PATH
EOF
if [[ -n "$SSH_PORT" ]]; then
cat <<EOF
ssh port: $SSH_PORT
EOF
fi
if [[ -z "$SSH_PORT" ]]; then
SSH_PORT=22
fi
ret_code=0
while true
do
if [[ $ret_code -eq 0 ]]; then
### 将删除操作也同步有一定危险性,确实需要的话,将下行加到 rsync 选项中去
### --delete \
rsync -e "ssh -p $SSH_PORT" \
-lurpEAXogDth --progress \
--exclude '.*.swp' \
--exclude '.vscode' \
--exclude '.git' \
${USER_PATHS[@]}
# ${SRC_PATH[@]} "$DST_PATH"
else
echo "invalid code return from inotifywait $?" >&2
fi
inotifywait -r -e modify -e create -e move_self -e delete -e move ${USER_PATHS[@]:0:$DST_IDX}
echo 'hello'
ret_code=$?
sleep 1
done
注意
- 代码未经充分测试, 出现什么意外都有可能, 风险谁使用谁自担.
- 在监控文件时, 发现文件更改产生的是
move_self
事件, 从字面上着实理解不了, 但最终例子中还是加了-e move_self
参考
- [1] 为
rsync
指定端口 https://stackoverflow.com/questions/4549945/is-it-possible-to-specify-a-different-ssh-port-when-using-rsync - [2]
rsync
命令手册man:rsync(1)
- [3]
inotifywait
手册man:inotifywait(1)
- [4] Advanced Bash Scripting Guide