はじめに
この投稿で共有したい成果物としては、以下のコードになります。
#!/bin/bash
#
# already_uploaded
#
# パラメータ:
#
# $1: S3へ既にコピーされているか判定するファイルのパス
# (例: /var/contents/sports/baseball/20160807-001.mov)
#
# $2: 上記のファイルがS3に上がっている場合のURL
# (例: http://hoge-journal.s3.amazonaws.com/sports/baseball/20160807-001.mov)
#
already_uploaded() {
MD5_1=`openssl md5 $1 | awk '{print $2}'`
MD5_2=`curl -s --head $2 | grep 'ETag:' | tr -d \\\\r | sed -E 's/^.*"([^"]+)"$/\1/'`
if [ ${MD5_1} = ${MD5_2} ]; then
echo 'true'
else
echo 'false'
fi
}
どういう状況で上記のようなスクリプトを書く必要に迫られたかを書いた方が、意図が伝わりやすいと思ったので以下に書きましたが、やや長文となりました。ご了承ください。
※(2016/8/13追記) 上記のコードで、$2 はS3に上げたファイルのURLに限りません。レスポンスヘッダに、リソースのMD5ハッシュ値がETagヘッダで返されるような仕様のWEBサーバーへのURLであればOKです。(そういう意味では、本投稿のタイトルは少しおかしいかもしれないという気がしてきましたが、そのままにしておきます。)
Ⅰ.already_uploaded()を作る必要に迫られた経緯
とある報道系BtoCサイト(仮にhttp://hoge-journal.jp
とします。)の中の人の話です。この人は、サーバーサイドのエンジニアです。状況はこうです。
-
編集・制作部門が業務に使うサーバーの
/var/contents
というディレクトリの下に、カテゴリ(経済だったり、スポーツだったり。)を表すサブディレクトリがいくつかあり、その中に総数で数千〜1万数千個ぐらいの、動画や画像が置かれる。 -
毎晩定時に、シェルスクリプトで書いた夜間バッチがcronで動いて、これらの動画や画像のすべてがAmazon S3 に (
aws s3 cp
コマンドで)コピーされる。 -
このコピーによって例えば、
/var/contents/sports/baseball/20160807-001.mov
という動画ファイルは、S3に上げられて
http://hoge-journal.s3.amazonaws.com/sports/baseball/20160807-001.mov
というURLから閲覧可能になる。なので、サイトのページにこの動画を載せたいときには、
<video src="http://hoge-journal.s3.amazonaws.com/sports/baseball/20160807-001.mov"></video>
といった感じで、上記のS3に向いたURLが使われる。
4. 上記2.のバッチによってS3にコピーされた後、動画や画像はサーバーからは削除される。
ただし、/var/contents/sports/baseball
などのサブディレクトリや、/var/contents/
ディレクトリ自体は消されない。
5. 翌日の同時刻にバッチが起動するまでに、再び/var/contents
の下に多数の動画や画像を、コンテンツ制作スタッフが FTPで上げる。
6. 制作スタッフは複数いて自分たちが作ったコンテンツのうち、すでにS3に上がっているものはどれなのか管理しておらず、毎回のバッチ起動前に、S3に上がっていて欲しいものの全てが、/var/contents
に置かれる。
7. したがって、毎回のバッチ起動時に/var/contents
以下にある動画なり画像の中には、前回あるいはそれ以前のバッチ処理で、すでにS3に上がっているものも含まれている可能性がある。
8. また現状のバッチスクリプトの流れはおおよそ以下ようになっている。
(1) /var/contents
以下に存在する動画や画像ファイルの全てを一覧する find
を実行
(2) その find の結果を受け取って while で回しつつ、各行すなわち、画像または動画の1ファイルパスに対して 1件の aws s3 cp
コマンドを発行するというループにより、S3へのコピーを行っている。
9. 上記の1.〜8.から起こり得ることとして、例えば、ある日の晩にバッチが動く時、/var/contents
の下に、全部で動画や画像が8,503 個あり、そのうち新しく作ったものと、ファイル名は同じで修正されたものは計10個しかなかったとしても、現状では、このバッチは(律儀に)8,503回のaws s3 cp
を実行する(が、本当に必要なコピーは、実はそのうちの10回)。
・・・と、このようなかなり無駄なコピーをしている可能性があった。
なので、バッチにちょこっと手を入れて、すでにS3に上がっている動画なり画像は、aws s3 cp
しない
ようにしたかった。
Ⅱ.具体的なお題
下記の要件を満たす、シェル関数already_uploaded
を作る。
現状のシェルスクリプト
上記Ⅰ.の経緯の中で、
8.また現状のバッチスクリプトの流れはおおよそ以下ようになっている。
(1)・・・
(2)・・・
と書きましたが、この部分は、具体的には以下のように書かれています。
cat ${FIND_CONTENTS_RESULT} |
while read FILE_PATH
do
# ${FILE_PATH}から、S3_BUCKET_PATH を作る。(詳細は省略)
S3_BUCKET_PATH=`make_s3path ${FILE_PATH}`
# S3へのコピーを実行
aws s3 cp ${FILE_PATH} ${S3_BUCKET_PATH} --acl public-read
done
上記のコード中、${FIND_CONTENTS_RESULT}
は find コマンドの結果がリダイレクトされたテキストファイルの
パスで、 例えば/tmp/contents-list-20160808-0200.txt
のようなテンポラリファイルのパスが入っている。
もし、8,503 個の画像や動画があるなら、/tmp/contents-list-20160808-0200.txt
は 8,503行になる。
wihleループが回って${FILE_PATH}
には、実際には
/var/contents/sports/baseball/20160807-001.mov
のような、コンテンツのフルパスが入り、${S3_BUCKET_PATH}
には、対応するコピー先のS3バケットのパスが入る。
上げる前の判定関数 already_uploaded を作りたい
現状、上記のようになっているのを、s3 へのコピーを行う直前で「それって、すでにうpされてんじゃね?」
という判定をする関数already_uploaded
を呼ぶようにしたい。この関数は、true
またはfalse
という文字列をecho するものとし、これがfalse
をechoしたときのみ、s3 への転送を行うようにして、以下のようにしたい。
if [ `already_uploaded ${FILE_PATH} ${S3_URL}` = 'false' ]; then
aws s3 cp ${FILE_PATH} ${S3_BUCKET_PATH} --acl public-read
fi
なお、上げるかどうかの判定対象の${FILE_PATH}
が示す動画なり画像が、すでにS3に
上がっていた場合に、それをWEBブラウザから参照するURLであるところの${S3_URL}
は、
どのコンテンツファイルについても、単純で同一のルールで簡単に作ることができるものとする。
すなわち、例えば${FILE_PATH}
が、/var/contents/sports/baseball/20160807-001.mov
であるならば、${S3_URL}
は、http://hoge-movies.s3.amazonaws.com/
の後ろに
sports/baseball/20160807-001.mov
を付加した、
http://hoge-journal.s3.amazonaws.com/sports/baseball/20160807-001.mov
となることを前提とする。
Ⅲ.解法の一例
幾つかの方法が考えられそうですが、http://hoge-journal.jp
の中の人は、S3からのレスポンスにETagヘッダが付加されることを使いました。(以下、様々なエラーに対応するためのコードは書いておらず、本筋だけを書いています。)
#!/bin/bash
#
# already_uploaded
#
# パラメータ:
#
# $1: S3へ既にコピーされているか判定するファイルのパス
# (例: /var/contents/sports/baseball/20160807-001.mov)
#
# $2: 上記のファイルがS3に上がっている場合のURL
# (例: http://hoge-journal.s3.amazonaws.com/sports/baseball/20160807-001.mov)
#
already_uploaded() {
MD5_1=`openssl md5 $1 | awk '{print $2}'`
MD5_2=`curl -s --head $2 | grep 'ETag:' | tr -d \\\\r | sed -E 's/^.*"([^"]+)"$/\1/'`
if [ ${MD5_1} = ${MD5_2} ]; then
echo 'true'
else
echo 'false'
fi
}
上記のコードの説明ですが、やっていることは、以下の3つ:
(1) アップロードしようとしているファイルのMD5ハッシュ値をopensslで求める。openssl md5
の出力が以下のようなものだった。
$ pwd
/var/contents
$ which openssl
/usr/bin/openssl
$ openssl md5 sports/baseball/20160807-001.mov
MD5(sports/baseball/20160807-001.mov)= xxxxxxxxxxxxxxxxxxxxxxxxxxxx
$
上記でいえば、xxxxxxxxxxxxxxxxxxxxxxxxxxxx
だけを取り出してMD5_1
に入れるために awk を使っている。
MD5(sports/baseball/20160807-001.mov)=
と、xxxxxxxxxxxxxxxxxxxxxxxxxxxx
の間にスペースが
あるので、awk '{print $2}'
で取れる。
(2) アップロードされていると仮定して、そのURLにcurlでHEADメソッドを送る。ヘッダを見たいだけなので、画像本体のデータは送られてこなくていいので、HEADメソッドで用が足りる。すると、以下のようなレスポンスが返ってくる。
x-amz-id-2: hs2QX6LYBqoQtO2CH0WI3CfaDbvkXtyWcp1i0klnOlsqwP4wD9IxlOcjKC7tFycToaU7VWcKJ30=
x-amz-request-id: FBB5041AA4B9F984
Date: Mon, 01 Aug 2016 13:24:33 GMT
Last-Modified: Thu, 16 Jun 2016 19:34:08 GMT
Server: AmazonS3
ETag: "yyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 248289
上記の中で、ダブルクオートで囲まれたETagヘッダの値だけを取り出すために
MD5_2=`curl -s --head $2 | grep 'ETag:' | tr -d \\\\r | sed -E 's/^.*"([^"]+)"$/\1/'`
としている。(ちなみに上記のパイプで、grep
とsed
との間に、tr -d \\\\r
をはさんで、キャリッジ・リターンを削除しないとsed
がちゃんと動かなかった。たぶんこれは環境によって違う。)
(3) 両者のMD5ハッシュ、MD5_1
とMD5_2
が一致するかしないかで、true
またはfalse
をechoする。
以下は補足:
※ curl -s --head $2
の出力から、ETagの値だけを取り出す方法は他にもいろいろあるでしょう。
※ 環境によっては、ファイルのMD5値を得るのに、md5sum
コマンドを使えるかもしれません。
※ awk 使ったり、 tr -d で \r を削除したりなどが必要かどうかは、環境によるかもしれません。
上記のalready_uploaded
の使い方として、「Ⅱ.具体的なお題」で書いた例で言うと、
FILE_PATH
が /var/contents/sports/baseball/20160807-001.mov
で、
S3_URL
がhttp://hoge-journal.s3.amazonaws.com/sports/baseball/20160807-001.mov
となっている状況で、
already_uploaded ${FILE_PATH} ${S3_URL}
とすると、もしすでに、sports/baseball/20160807-001.mov
がS3に上がっていて、かつ、上げようとしているサーバ側のファイルと中身も同じであれば、true
がechoされ、そうでないならfalse
がechoされる。
以上、「Amazon S3に何かをコピーするときに、すでに同じものをコピー済みかどうかをチェックするシェル関数」を共有させて頂きました。
お読みいただき、ありがとうございます。