Shell:异构目录的文件双向同步(一)

原创
2021/08/08 16:47
阅读数 130

故事的开头

拿到一批100G+的资料,分别拷贝在了多个U盘里,按理说内容应该一模一样,打开扫了几眼,没到存在不一样的内容。再仔细一看,应该是不同人操作的,有些文件内容一致但细节不同,比如同一个视频内容的字幕大小不同。显然是不同来源的文件,由不同的人分工拷贝的。

问题出现:如何得出两个U盘上资料的并集,并同步至这两个U盘上?

小猪过河

假设其中一个U盘资料为集合A,另一个为集合B,则只要把A-B拷贝到B,把B-A拷贝到A就行了,即A和B的并集。

说人话就是,把A盘上B没有的拷到B盘,把B盘上A没有的拷到A盘,从而实现共赢,皆大欢喜!

如何找到两个盘上的不同文件?

既然用shell实现,小猪佩奇自然要用最简单粗暴的方法(one-liner):

dirA=${1:-"/run/media/bing/D20D/"}
dirB=${2:-"/run/media/bing/1234/"}
fd -t f . "$dirA" "$dirB" -x md5sum {} | tee f.txt | awk '{ a[$1]++ }; END{ for(i in a){if(a[i]==1)print i} }' | xargs -i grep "^{}" f.txt

列出两个盘 dirAdirB 上的所有文件,并找出唯一md5值的文件

说明:

  • fd:find 命令的 rust 版,简洁快速
  • -t f:普通文件
  • .:任意文件通配符
  • -x:对查找结果执行命令,其中{}将被替换为结果
  • tee f.txt:把 md5 值保存到文件,并继续处理
  • awk:统计每个 md5 值的重复次数,并打印其中唯一的

期待是,如果有独一无二md5的文件,自然只有一份,那是需要同步的。

愿望很美好,但这个命令没能执行完,因为夜太漫长。。。

对所有文件做checksum实在太慢!不仅文件数量多,且U盘读取慢。

MISSION FAIL!

小马过河

小马宝莉目测两个U盘大概有80%+的内容完全一样,反思了一下之前的方案:

  • 可以先比较文件大小+名称,如果都一样,则认为是相同文件
  • 如果大小相同,名称不同,再考虑checksum,能大大节省时间

1. 文件列表新方案

fd -t f . "$dirA" "$dirB" -x sh -c 'stat --printf=%s "{}"; echo -e "\t{}"' | sort -o f.txt
  • sh -c:能执行多条命令
  • stat:用来获取文件大小,比 ls 好用
  • echo -e:能打印转义字符 \t
  • sort -o:排序并保存到文件

输出类似:

1077382	/run/media/bing/1234/资料100g/电子书/xyyh闲云野鹤却对世界保有热情.pdf
1077382	/run/media/bing/D20D/书/闲云野鹤却对世界保有热情.pdf

其中,D20D是A盘,1234是B盘。

2. 核心比较函数

接下来就是先比较文件大小+名称,再比较检校值

# bash 的关联数组相当于 python 的 map,但只能保存字符串
# 使用两个关联数组:
# fileSet={size: path},用来快速比较文件大小+名称
# fileStat={path: mark},标记需要同步的文件
# 把比较和结果分开,逻辑会清晰很多!
declare -A fileSet fileStat
cat f.txt | while read -r line; do
    size=$(cut -f1 <<< "$line")
    path=$(cut -f2- <<< "$line")
    # 相同大小
    if [[ ${fileSet[$size]+_} ]]; then
        fpath=${fileSet[$size]}
        # 相同文件名
        if [[ $(basename "$fpath") == $(basename "$path") ]]; then
            unset fileStat[$fpath] fileStat[$path]  #去除相同文件
            continue
        fi
        # 相同检校值
        if [[ $(get_file_sum "$fpath") -eq $(get_file_sum "$path") ]]; then
             unset fileStat[$fpath] fileStat[$path]  #去除相同文件
        fi
    else
        fileSet+=([$size]="$path")
        fileStat+=([$path]="")
    fi
done
  • declare -A fileSet fileStat:定义两个关联数组

  • [[ ${fileSet[$size]+_} ]]:测试数组是否包含某个 key

  • ${fileSet[$size]}:数组取值

  • unset fileStat[$fpath] :删除 key

  • fileSet+=([$size]="$path"):添加新的键值对

其中,获取检校值函数 get_file_sum 改用了 crc32 方法:

# crc32 好像比 md5 要快一点
get_file_sum() {
    sum -s "$1" | cut -d' ' -f1
}
# sum -s 202006.mp3 
43932 41808 202006.mp3   # crc值  文件块数  文件路径

3. 保存过滤后结果

fileStat保存了剩余不同文件的路径,可以输出整理了:

: > d.txt
for path in "${!fileStat[@]}"; do
    echo "$path" 
done | tee -a d.txt
  • : > d.txt:冒号代表 true,无输出。通过重定向清空文件
  • ${!fileStat[@]}:获取所有的 key
  • tee -a d.txt:显示输出,并以追加的方式保存到文件

接下来是漫长的等待,用 htop 看了下进程,发现时间基本都耗在读取U盘文件,并计算检校值上了,嗯嗯,可以理解。

About 1 hour later

结果预处理

d.txt中输出了一大堆路径,200+行,因为两个盘上的目录结构不同(显然由不同人整理拷贝),接下来不得不手工整理文件列表。其实命名上有一定的相似性,比如:

/run/media/bing/1234/资料100g/音频12g/众里寻你.mp3
/run/media/bing/D20D/01论道/略论1-29课/21.mp4
/run/media/bing/D20D/书/意识和潜意识.pdf
/run/media/bing/1234/资料100g/音频12g/有声书/rxl.mp3
/run/media/bing/1234/资料100g/论道25g/01略论29课10g/21.mp4

路径看上去很乱,整理很困难,只能试着先排序,最好是两个盘上的相似文件排在一起,方便确认整理。试过按文件大小排序、直接按路径排序等,都不理想。

又想了下,根据结果,有不少文件都在同一个文件夹,其实比较自然的还是按文件夹来排序,这样方便批量处理,而且双方的命名有相似性。

# 根据文件所在的文件夹排序
: > dd.txt
cat d.txt | while read -r path; do
	dir=$(basename "${path%/*}")
        echo -e "${dir}\t${path}"
done | sort | cut -f2 | tee -a dd.txt
  • ${path%/*}:删除结尾的文件名
  • basename "${path%/*}":获取末尾的文件夹
  • sort | cut -f2:排序后再删掉文件夹名

结果终于能看了!

结果标记

因为目录异构,无法直接确认要拷贝的目的路径,还是人工来吧,但一个文件一个文件写明拷贝目的路径显然 mission impossible,按文件夹批量会快点。

由此设计了文件操作标记,直接标在了文件列表上。因为shell一般按行读取文件,所以称为流式标记,一个操作一行(顺序很重要),方便编程处理。

-/run/media/bing/D20D/01论道/略论1-29课/21.mp4
=>/run/media/bing/D20D/01论道/略论1-29课/
/run/media/bing/1234/资料100g/论道25g/01略论29课10g/21a.mp4

=>/run/media/bing/D20D/音频/
++有声书
/run/media/bing/1234/资料100g/音频12g/有声书/rxl.mp3
/run/media/bing/1234/资料100g/音频12g/有声书/nmw.mp3
  • -开头的是要删除的文件
  • =>拷贝目的路径
  • ++需要在当前目的路径下新建某个目录
  • /开头的是待拷贝文件/文件夹
  • 遇到空白行,重置各变量,防止拷错地方

标记完成(s.txt),大概花了40~60分钟。如果是整个文件夹拷贝,很多行可以删掉,结果又精简不少。

最后同步文件

根据标记,解析处理:

local dst new_dir
cat s.txt | while read -r L; do
    case "$L" in
        -* )
            echo rm -f "${L#-}"
            continue ;;
        '=>'* )
            dst=${L#=>}
            continue ;;
        ++* )
            new_dir=${L#++}
            dst="${dst%/}/${new_dir}"
            echo mkdir -p "${dst}"
            continue ;;
        /* )
            echo cp -rup "$L" "$dst"
            continue ;;
        '' )
            dst=''
            new_dir=''
            continue ;;
        * )
            echo "Unknow format: $L"
            exit 3 ;;
    esac
done
  • cp -rupr 支持文件夹;u 仅 update,不重复拷贝;p 保留时间戳
  • echo:加在文件操作前,是为了提前观察一下有没有问题,因为一旦操作了,难以撤销,建议多加些检查。

执行完成,搞定收工!😃

危机再现,未完待续

  • 换了个U盘同步,发现主目录居然和上一个不一样?!难道又要重新跑一遍?
  • 发现最后的同步文件列表还是有些问题,代码哪里有问题?
展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部