Linux 简易目录同步工具实现

Linux 简易目录同步工具实现

快速实现功能类似 WinSCP 的同步工具.

基本原理是:

  1. inotifywait 命令监控目录的变化;
  2. rsync 实现增量同步;

inotifywait 是监控涉及文件的操作的事件的, 我们利用此命令监控需要同步的目录,
有增/删/改等操作时, 命令就返回.

rsync 基本用法见其 man 文档, 本处使用的是 rsync [OPTION...] SRC... [USER@]HOST:DEST .

实现要求

  1. 双方都是 Linux 系统.(实际发送方有inotifywaitrsync 工具, 且接收方有 ssh 服务器 的应该都行, 但未测试)
  2. 为了能让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 使用的命令说明:

  1. inotifywait
    1. -r 选项是监控目录之内所有内容(包括文件及子目录),
    2. -e <event> 是指定文件系统事件, 见参考文档[3]. 如果不确定事件对应哪种操作, 可使用 inotifywait -r -m /path/to/foo/ 命令监控 foo 目录, 再进行些文件操作, 如创建,删除等, 以查看产生的事件名称. 示例中列出事件应该是全了.
  2. rsync'ssh -p 20052' 是指定端口的选项. (本处是从物理机同步到虚拟机中)

优化实现

  1. 实际使用中, 本地删除的文件, 目标服务器上也应该删除, 此时应给 rsync 增加 --delete 选项. 因多数是同步源码使用, 加此选项的风险用户自负.
  2. 另外, 文件时, 有些目录或文件是不需要的, 如 vim 会产生个 .xxx.swp 文件, 此时用 rsync--exclude '.*.swp --exclude '.vscode' --exclude '.git'' 来排除. .具体见文档
  3. 脚本对监控目录及目标目录写死了,不灵活, 使用命令选项比较好.
  4. ...

最终更改:

#!/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

注意

  1. 代码未经充分测试, 出现什么意外都有可能, 风险谁使用谁自担.
  2. 在监控文件时, 发现文件更改产生的是 move_self 事件, 从字面上着实理解不了, 但最终例子中还是加了 -e move_self

参考

posted @ 2021-08-12 22:34  陈tseesing  阅读(408)  评论(0编辑  收藏  举报