掘金 后端 ( ) • 2024-04-02 19:08

笔者在其他的博文里面,讨论了关于文件分割与合并的程序化操作。这些操作在一些文件备份、归档和传输的场合可能会有一些应用。但实际上,如果没有其他的特别需求,最简单的处理方式其实是使用操作系统的相关命令和机制来实现这个需求。

所以,本文就作为一个番外小品,补充分析和记录一下在操作系统中,使用原生的文件系统功能和命令,在CLI(Shell)环境中,如何通过命令和简单的编程,来实现这些操作的过程。

方便讨论和检验起见,本文中的所有操作,都基于linux标准系统和标准命令。

文档分割

文档分割,通常使用split命令。它有一些选项,如可选分割的字节大小、行数量和分割数量等等。本文中可以使用固定字节大小的形式,可以以二进制方式处理任意类型的文件。其标准形式是:

split 1.mp4 -b 2M 1.mp4.

这里的第一个1.mp4是要分割的文件名称;-b是按字节数分割,每个块大小为2M;1.mp4.分割后的文件名称前缀,然后按照aa、ab等等序号命名。

笔者在此基础上,编写了一个简单的shell脚本文件,可以分割文件,并使用其sha1值对每个分割文件进行重命名:


#!/bin/bash

split "$1" -b 2M "${1}."

## files = echo "${1}.*"  2>/dev/null

for f in $(echo "$1.*"); do
    ## hash =  echo "$file  handle"
    sha1sum=$(sha1sum "$f" | cut -d ' ' -f 1)
    nf="${f}.${sha1sum}"

    mv "$f" "$nf"
    ## sha1sum $f
done

文档合并

文档合并使用cat命令,其标准形式为:

ls 1.mp4.* cat 1.mp4.* > 1R.mp4

这个命令,会将所有1.mp4.为开始的文件进行遍历,并将它们连接(concat)起来,并且输出成为一个文件。主要注意,cat命令可以有序的支持多个文件,所以,如果使用通配符,要注意分片文件的顺序和完整性。在合并操作之前,最好使用ls命令检查一下。

如果使用前面脚本进行的文件分割脚本,并且希望在合并时,使用SHA校验机制,则需要编写对于的合并检查和操作脚本,参考代码如下:


#!/bin/bash
for f in $(echo "$1.*"); do
    h1=$(sha1sum "$f" | cut -d ' ' -f 1)
    h2=$(echo $f | cut -d '.' -f 4)

    if [ "$h1" != "$h2" ]; then
       echo "SHA Value Not Match, Exit!!"
       exit 1
    fi
done


cat $(echo "${1}.*") > "$1._RESTORE"

echo $(sha1sum "$1._RESTORE")

这个脚本将会基于给定的分割文件的前缀,在当前文件夹中遍历已分割的文件片段,并且检查它们的完整性。如果不能通过检查,则会提示错误信息并且退出程序;如果通过检查,则将它们合并成为一个 "._RESTORE"文件。并输出SHA计算结果。调用和执行过程如下:


[yanjh@john-eosdev test]$ ./ct.sh 1.iso
2299d4730a7a41609cd32570b32e6d3345b88267 1.iso._RESTORE

完整性

要保证文件分割和合并后,文件的完整性和一致性,可以使用哈希方法进行校验。linux系统内置了相关的校验命令包括:sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, shasum 等等。我们以sha1sum为例,检查文件分割和合并前后的哈希值,相关的操作和信息如下:


[yanjh@john-eosdev incoming]$ sha1sum 1.mp4
81b04ac6d90884960e74038af9aab17cfa8e5c60  1.mp4
[yanjh@john-eosdev incoming]$ sha1sum 1R.mp4
81b04ac6d90884960e74038af9aab17cfa8e5c60  1R.mp4

对于相关哈希计算的特性,作为开发者我们应该理解:

  • 文件的哈希值计算,和文件名称无关,只和文件的内容有关
  • 任何修改文件的内容,都会导致后续计算的哈希值不一致,所以哈希值一致,可以确保内容的一致
  • 本质上而言,哈希计算处理的是二进制数据,和文件类型、结构和规模都无关

但作为开放式的算法,攻击者可以自行构造内容和对应摘要值,来伪造文件的内容,当然,他需要两个信息都同时提供给信息校验者,并让其相信这些信息的来源没有问题。所以,对应要求的信息安全场合,还可以使用私钥签名公钥验证的方式。这种方式实现和操作代价比较高。简单的方式可以使用linux内置的hmac256命令,可以简单的理解就是对摘要计算加了一个密码,从而实现发行方自己的校验,示例如下:


[yanjh@john-eosdev incoming]$ hmac256 "mysecret" 1.mp4
82aa2fb61fa7709f94eb20c93720483e3f4e04ec4788200bdac0c081862967e0  1.mp4
[yanjh@john-eosdev incoming]$ hmac256 "mysecret" 2.mp4
82aa2fb61fa7709f94eb20c93720483e3f4e04ec4788200bdac0c081862967e0  2.mp4

操作性能

为了让读者对于文件分割和合并操作的性能有一个基础的概念,笔者使用上面章节涉及到的命令和脚本,笔者在自己的开发用计算机系统上进行了简单的性能测试。操作环境的软硬件配置是:

  • CPU: Intel I5 3550S
  • RAM: DDR3 1600 8G
  • HDD: Hitachi Ultrastar 3TB 64MB 6Gb/s SATA 3.5寸企业级机械硬盘
  • OS: EndeavourOS Linux/Linux 6.7.8-arch1-1
  • 测试文件: 1.iso, 3414891436 byte, 分割大小 2M

测试结果为:

  • 文件分割操作(带SHA1计算): 94s
  • 文件合并操作: 39s
  • 整个文件SHA1SUM: 18~24s
  • 如果将分割大小改为20M,分割合并的测试结果分别为: 53s和24s
[yanjh@john-eosdev test]$ ls -l
总计 3334876
-rw-r--r-- 1 yanjh yanjh 3414891436  4月 2日 10:07 1.iso

[yanjh@john-eosdev test]$ time ./sp.sh 1.iso
real    1m33.673s
user    0m10.861s
sys     0m8.983s

[yanjh@john-eosdev test]$ time cat 1.iso.* > 2.iso

real    0m38.925s
user    0m0.014s
sys     0m3.305s

[yanjh@john-eosdev test]$ time sha1sum 1.iso
2299d4730a7a41609cd32570b32e6d3345b88267  1.iso

real    0m17.760s
user    0m8.149s
sys     0m1.550s
[yanjh@john-eosdev test]$ time sha1sum 2.iso
2299d4730a7a41609cd32570b32e6d3345b88267  2.iso

real    0m24.345s
user    0m9.807s
sys     0m2.241s

从操作过程和理论上而言,我们可以看到这个操作对于CPU和内存都基本上没有压力,主要的瓶颈在于磁盘IO性能。笔者专门选择了机械硬盘,可以看作可以对这种操作的一个基线性能的一个了解。

shell遍历通配符的坑

笔者在实践以上操作的时候,曾经遇到过这么一个问题。就是如果使用一个通配符,来对文件进行遍历,在CLI中是没有问题的,但如果将操作写到SHELL脚本中,就出了问题,表现为只能操作处理第一个文件项目。

这个问题,笔者的进行了一些搜索和研究,都没有一个明确的说法。包括ChatGPT和Claude,提出的技术方案和代码,都没有明确的说明这个信息,处理的结果也是错误的。但有一点点线索,引发了笔者的思考和猜想。就是“通配符”或者模式,是在shell层面进行处理的,就是说,在执行shell脚本的时候,shell会先解析脚本参数中的通配符,然后作为参数列表传入脚本进行处理,而不是将带有通配符的字符串,作为参数传入脚本进行处理(这一般是理想和期望的情况)。

下面有一个简单的示例,方便大家理解这个问题:

// l.sh 内容

#!/bin/bash

files=$(ls $1 2>/dev/null)
for file in $files; do
    sha1sum $file
done

// 错误的调用方式

[yanjh@john-eosdev incoming]$ ./l.sh 1.mp4.*
b86016116e05f11886f34a7d53c204508ea9d372  1.mp4.aa.b86016116e05f11886f34a7d53c204508ea9d372

// 正确的调用方式
[yanjh@john-eosdev incoming]$ ./l.sh "1.mp4.*"
b86016116e05f11886f34a7d53c204508ea9d372  1.mp4.aa.b86016116e05f11886f34a7d53c204508ea9d372
2a4b355b5aab371f18cf6f86d7475e9834a45638  1.mp4.ab.2a4b355b5aab371f18cf6f86d7475e9834a45638
ec8d4e50b1bee08fc60ddc48eb49ce72ff2e2b90  1.mp4.ac.ec8d4e50b1bee08fc60ddc48eb49ce72ff2e2b90
e37695f8c0eb851c98f600d0f08febd255e86346  1.mp4.ad.e37695f8c0eb851c98f600d0f08febd255e86346
250446e060e89b6b89ac91f80c84e6b6eadd4dcd  1.mp4.ae.250446e060e89b6b89ac91f80c84e6b6eadd4dcd
a5b434ecb7f39f7abf5fe923f7106f81a8881cf4  1.mp4.af.a5b434ecb7f39f7abf5fe923f7106f81a8881cf4
2ef487279cb07a78657f396734050c4d820c5bfe  1.mp4.ag.2ef487279cb07a78657f396734050c4d820c5bfe
b79e61f89244483dc0c80f3f580d0fa34a4e94a8  1.mp4.ah.b79e61f89244483dc0c80f3f580d0fa34a4e94a8
b90f3f6523a998ca5f3c56ac6f2703b7ddc7e571  1.mp4.ai.b90f3f6523a998ca5f3c56ac6f2703b7ddc7e571
5b618a39cb846f8fece7c87846efb8a9be8744f4  1.mp4.aj.5b618a39cb846f8fece7c87846efb8a9be8744f4


所以,上面的案例已经说明了通配符作为普通参数和文本化的参数,对于脚本内部的处理方式的不同。所以,在这种应用场景中,笔者认为,这个问题的比较好的解决方案是:

无需在脚本的层面解决这个问题,只能在执行的时候,不直接使用默认的参数形式,而是需要将参数使用引号封装起来作为文本参数传入脚本来进行处理

另外, 这里笔者想要再次抱怨一下,作为一个脚本化的语言,shell脚本的容错性比较差。例如在脚本中,多一个空格,解释器可能都不能很好的处理掉(如 h1=xx,就不能写出h1 = xxx),而且它的错误提示信息也不是那么明确,完全要靠开发者的经验和细心,这些都会比较大的影响开发和调试的效率。

小结

本文讨论了使用操作系统内置的文件分割和合并命令,来进行文件操作的一般过程,并扩展讨论了相关操作的脚本化,文件信息完整性验证和性能相关的问题。