Help us understand the problem. What is going on with this article?

Amazon S3に何かをコピーするときに、すでに同じものをコピー済みかどうかをチェックするシェル関数

More than 3 years have passed since last update.

はじめに

この投稿で共有したい成果物としては、以下のコードになります。

#!/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 とします。)の中の人の話です。この人は、サーバーサイドのエンジニアです。状況はこうです。

  1. 編集・制作部門が業務に使うサーバーの/var/contents というディレクトリの下に、カテゴリ(経済だったり、スポーツだったり。)を表すサブディレクトリがいくつかあり、その中に総数で数千〜1万数千個ぐらいの、動画や画像が置かれる。

  2. 毎晩定時に、シェルスクリプトで書いた夜間バッチがcronで動いて、これらの動画や画像のすべてがAmazon S3 に (aws s3 cpコマンドで)コピーされる。

  3. このコピーによって例えば、
    /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/'`

としている。(ちなみに上記のパイプで、grepsedとの間に、tr -d \\\\r をはさんで、キャリッジ・リターンを削除しないとsedがちゃんと動かなかった。たぶんこれは環境によって違う。)

(3) 両者のMD5ハッシュ、MD5_1MD5_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_URLhttp://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に何かをコピーするときに、すでに同じものをコピー済みかどうかをチェックするシェル関数」を共有させて頂きました。
 お読みいただき、ありがとうございます。

jun68ykt
ときどき teratail で回答してます。
https://teratail.com/users/jun68ykt#tag
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away