技研のまつけんです。

 

前回の記事では、複数のアーカイブを展開してファイルやフォルダの名前の傾向を調べる過程を紹介しました。今回は、その逆の「多数のファイルを分活してアーカイブする」ためのシェルスクリプトを紹介したいと思います (前回の「まとめ」では違う予告をしましたが、その前に今回の作業をする機会があったので、先に記事にしました)。

自分がデータクレンジングしたものを誰かに渡すことがあります。その際、メールで送るにしてもサーバに置くにしても、再びアーカイブ (+圧縮) すると便利なのですが、数ギガバイトのzipやtgzだと色々と不便です。そこで、分割してアーカイブしたくなります。それを実現するのに、真っ先に思いつく方法は、a、b、cという3つのフォルダがあるのであれば (そして、3つのフォルダ内のデータ量に偏りが無いのであれば)、

tar cfz a.tgz data/a*
tar cfz b.tgz data/b*
tar cfz c.tgz data/c*

のように

  • aで始まるものは、a.tgz
  • bで始まるものは、b.tgz
  • cで始まるものは、c.tgz

としてしまうことでしょう。

しかしながら、いつもこのように綺麗に分かれているとは限りません。そうすると、どのように分ければ良いか検討しなければなりません。これを手作業でするとなると意外に面倒です。そこで、今回は、最終的なアーカイブファイルの個数の上限を指定すると、分け方を考えてくれるスクリプトを作りました。こちらです:

TGT="$1"
LIM=10
SKIP=1
find "$TGT" -type d | grep -v ^"$TGT"$ > list
find "$TGT" -not -type d | grep -v ^"$TGT"$ > list_nd
LEN=`cat list | sed -e 's#.#x#g' | uniq | sort | uniq | tail -n 1 | wc -c`
declare NS
for N in `seq $(($LEN - 1))`
do
M=`cat list | cut -c -$N | uniq | sort | uniq | wc -l`
NS[$M]=$N
if [ $LIM -lt $M ]
then
break
fi
NCH=$N
NFL=$M
done
for M in "${!NS[@]}"
do
echo -n "${NS[$M]} chars: $M files"
if [ ${NS[$M]} -eq $NCH ]
then
echo " <<< Selected <<<"
else
echo ""
fi
done
while read PREFIX
do
ITEMS=`cat list_nd | grep ^"$PREFIX" | wc -l | sed -e 's#...$#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#,\+#,#g' -e 's#^,##'`
echo "$PREFIX* -> "`basename "$PREFIX"`.tgz" ($ITEMS items)"
done < <( cat list | cut -c -$NCH | uniq | sort | uniq )
echo -n "$(tput cuu $NFL)"
while read PREFIX
do
ARC=`basename "$PREFIX"`.tgz
ITEMS=`cat list_nd | grep ^"$PREFIX" | wc -l`
ITEMS2=`echo $ITEMS | sed -e 's#...$#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#,\+#,#g' -e 's#^,##'`
echo -n "$PREFIX* -> $ARC ($ITEMS2 items) "
echo -n "$(tput sc)"
if [ $SKIP -ne 0 -a -f "$ARC" ]
then
echo -n "<<< Comparing # of items <<<"
ITEMS_ALL=`cat list list_nd | grep ^"$PREFIX" | wc -l`
ITEMS_ARC=`(tar tfz $ARC | wc -l) 2> /dev/null`
else
ITEMS_ALL=-1
ITEMS_ARC=-2
fi
echo -n "$(tput rc)"
echo -n "$(tput el)"
if [ $SKIP -ne 0 -a $ITEMS_ALL -eq $ITEMS_ARC ]
then
echo -n "Skipped "
else
echo -n "<<< Processing <<<"
tar cfz "$ARC" "$PREFIX"*
echo -n "$(tput rc)"
echo -n "$(tput el)"
echo -n "Done "
fi
SIZE=`wc -c < "$ARC" | sed -e 's#...$#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#...,#,\0#' -e 's#,\+#,#g' -e 's#^,##'`
echo "($SIZE bytes)"
done < <( cat list | cut -c -$NCH | uniq | sort | uniq )
rm -f list list_nd

今回のような処理を自動で行おうと考えた場合、もっとも簡単な方法は「先頭から何文字までを共通として扱えば目的を達成できるか」を調べることでしょう。例えば、前回の記事で扱った例とよく似たフォルダ構成:

data_000/20101121-090928
data_000/20110301-011826
data_000/20110502-195236
data_000/20110707-085504
data_000/20120407-180406
data_000/20120509-133753
data_000/20120513-204704
data_000/20120725-104143
data_000/20120824-024826
data_000/20121116-160047

のような場合、「data_000/20*」は上記の全ての項目を指しますが、先頭の12文字「data_000/201」までを共通として扱い、13文字目の違いを利用して

tar cf 2010.tar data_000/2010*
tar cf 2011.tar data_000/2011*
tar cf 2012.tar data_000/2012*

とすれば、3つのファイルにわけてアーカイブすることが出来ます。13文字目までを共通として扱い、14文字目の違いを利用して

tar cf 20101.tar data_000/20101*
tar cf 20110.tar data_000/20110*
tar cf 20120.tar data_000/20120*
tar cf 20121.tar data_000/20121*

とすれば、4つに分かれます。

こちらに対して実行した結果です。そのときの実行中の画面表示がこちらです:

12 chars: 1 files
13 chars: 3 files
14 chars: 4 files
15 chars: 9 files
25 chars: 10 files <<< Selected <<<
26 chars: 28 files
data_000/20101121-090928_* -> 20101121-090928_.tgz ( 7,091 items) Skipped (69,986,411 bytes)
data_000/20110301-011826_* -> 20110301-011826_.tgz (10,856 items) Skipped (155,913,511 bytes)
data_000/20110502-195236_* -> 20110502-195236_.tgz (12,824 items) Skipped (189,698,173 bytes)
data_000/20110707-085504_* -> 20110707-085504_.tgz (12,912 items) Done (183,731,059 bytes)
data_000/20120407-180406_* -> 20120407-180406_.tgz (12,160 items) Done (176,292,719 bytes)
data_000/20120509-133753_* -> 20120509-133753_.tgz ( 1,672 items) Done (25,096,677 bytes)
data_000/20120513-204704_* -> 20120513-204704_.tgz (10,080 items) Done (146,139,693 bytes)
data_000/20120725-104143_* -> 20120725-104143_.tgz (10,160 items) <<< Processing <<<
data_000/20120824-024826_* -> 20120824-024826_.tgz ( 3,896 items)
data_000/20121116-160047_* -> 20121116-160047_.tgz (14,962 items)

最初の部分は、何文字目まで共通にすると幾つのファイルが出来るか、のリストです。冒頭で、LIM=10としているので、10ファイル以下におさまる最大の値として、「25文字目まで共通」が選ばれています。実行中のステータスの表示などについては、カーソルを移動 [1][2] して上書きすることで実現しています。途中でctrl-cなどで中断すると、その位置にプロンプトが出てしまうので、trap [3] を使って、後始末をするようにしても良いかと思いますが、今回は省略しています。また、プログレスバー [4] を表示するのも良さそうです。

サマリー情報

上に述べたように分割したファイルを受け渡す場合は、受け取った側がこちらと同じフォルダ・ファイル構成を再現できたのか、確認する手段が欲しいところです。そこで、サマリー情報を送る方法を紹介します。

以下の通り、アイテムのリストと、それぞれのファイルのMD5を記録することが出来ます:

find result | sort | gzip > items.gz
find result -type f | sort | xargs md5sum | gzip > file-sums.gz

受け側は、分割されたアーカイブを受け取って展開したあと、

diff <( find result | sort ) <( zcat items.gz )
find result -type f | sort | xargs md5sum | gzip > file-sums.gz

のようにすることで、全てのフォルダ・ファイルを正しく受け取ることが出来たか、確認することができます。

まとめ & Known Bug

如何でしたか? 今回は、大量のファイルを分割して送る方法、また、正しく受け渡せたのか確認する方法について紹介しました。よろしければ、活用してみてください。

さて、実は、今回のスクリプトにはバグがあります。実は、

./data/data_000/20101121-090928
./data/data_000/20110301-011826
./data/data_000/20110502-195236
./data/data_000/20110707-085504
./data/data_000/20120407-180406
./data/data_001/20120509-133753
./data/data_001/20120513-204704
./data/data_001/20120725-104143
./data/data_002/20120824-024826
./data/data_002/20121116-160047

のようなディレクトリ構成の場合、

12 chars: 1 files
13 chars: 3 files
17 chars: 6 files
18 chars: 8 files
19 chars: 9 files <<< Selected <<<
20 chars: 12 files
data/data_000* -> data_000.tgz
data/data_000/20101* -> 20101.tgz
data/data_000/20110* -> 20110.tgz
data/data_000/20120* -> 20120.tgz
data/data_001* -> data_001.tgz
data/data_001/20120* -> 20120.tgz
data/data_002* -> data_002.tgz
data/data_002/20120* -> 20120.tgz
data/data_002/20121* -> 20121.tgz

という動きをしてしまうのです。これでは、同じファイルが複数のアーカイブに入ってしまいます。これについては原因はわかっている (そして、それほど困っていない) ので、いずれ直したいと思いますが、読者の皆様もパズル感覚で修正案を考えていただければ、と思います。