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

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

```bash
#!/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から閲覧可能になる。なので、サイトのページにこの動画を載せたいときには、

```html
<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 で回しつつ、各行すなわち、画像または動画の１ファイルパスに対して　１件の ```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)・・・

と書きましたが、この部分は、具体的には以下のように書かれています。

```bash
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 への転送を行うようにして、以下のようにしたい。

```bash
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ヘッダが付加されることを使いました。（以下、様々なエラーに対応するためのコードは書いておらず、本筋だけを書いています。）

```bash
#!/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  

}
```

上記のコードの説明ですが、やっていることは、以下の３つ：

(1) アップロードしようとしているファイルのMD5ハッシュ値をopensslで求める。```openssl md5``` の出力が以下のようなものだった。

```shell-session
$ 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ヘッダの値だけを取り出すために

```bash
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```
となっている状況で、

```bash
already_uploaded ${FILE_PATH} ${S3_URL}
```
とすると、もしすでに、```sports/baseball/20160807-001.mov```がS３に上がっていて、かつ、上げようとしているサーバ側のファイルと中身も同じであれば、```true```がechoされ、そうでないなら```false```がechoされる。




　以上、「Amazon S3に何かをコピーするときに、すでに同じものをコピー済みかどうかをチェックするシェル関数」を共有させて頂きました。
　お読みいただき、ありがとうございます。