2015/06/11

SDKを使わずAmazon S3のファイル名を変更してダウンロードするスニペット

S3のファイルをダウンロードするとき、期限付きのURLやダウンロードファイルの名前を変更したりできるpre-signed URLという機能があります。

AWS系のライブラリを使えば簡単なのですが(参考)、使うと必要ないものが沢山ついてきてdeploy作業とかが重くなるのいやなので、シンプルにPure Pythonで書いたのでそのコードをココで公開しておきます。
※シンプルなコードなのでPythonじゃなくても真似すればどんな言語でも動きそう。


def generate_url_s3(bucket, path, output_filename):
    import time
    import hmac
    import hashlib
    import base64
    import urllib
    access_key = 'YOUR_AWS_ACCESS_KEY'
    secret_key = 'YOUR_AWS_SECRET_KEY'
    expire_in = 86400  # 1 day (60 x 60 x 24)
    expire = int(time.time() + expire_in)

    c_string = 'GET\n\n\n%d\n/%s/%s?response-content-disposition=attachment; filename="%s"' % (expire, bucket, path, output_filename)
    sign0 = base64.b64encode(hmac.new(secret_key, c_string, hashlib.sha1).digest())
    sign = urllib.quote(sign0, '')

    q = 'Signature=%s&Expires=%d&AWSAccessKeyId=%s' % (sign, expire, access_key)
    extra = 'response-content-disposition=%s' % urllib.quote('attachment; filename="%s"' % output_filename)
    url = "http://%s.s3.amazonaws.com/%s?%s&%s" % (bucket, path, q, extra)
    return url


このURLはGETリクエストのみに対応しています。
POSTに対応したい場合はc_string文字列の中のGETをPOSTとかにすると動くかもです(未確認)。
access_keyとsecret_keyはご自身のAWSのモノをご利用下さい。
c_stringはリクエストの内容の文字列で、これをSHA1でAWSのSecret Keyで暗号化した文字列をhmacでハッシュ化してHTTPリクエストの文字列に耐えられるようbase64エンコードと特殊文字のエスケープ(urllib.quote)により署名とします。
これらのエンコード処理を行わないと、SignatureにはURIパラメータに適さない文字列を作成することがあり、AWS側でそれをデコードしてしまいSignatureが一致しない「SignatureDoesNotMatch」というエラーが発生することがあります。

あとはリクエストURLを構築するだけです。
S3に格納されたファイルのベースのURLに署名や有効期限(本スニペットではURL作成時から1日)、AWSのAccess Keyを付け、最後にダウンロードファイル名の指定します。
これでOKです。
c_stringがちょっとクセモノですが、他の署名については一般的なOAuth2.0などの方法と同じでシンプルで分かりやすかったです。

使い方は簡単で例えば以下のような使い方です。
bucket = "mybucket"
path = "tmp/myfile.zip"
output_filename = "sample.zip"
url = generate_url_s3(bucket, path, output_filename)
print(url)

bucketにS3のバケット名を指定。
pathはダウンロードファイルのkey名です。
output_filenameはダウンロードファイル名を指定します。
この3つのパラメータから、期限付きかつダウンロードするファイル名を変更したURLを生成できます。

たった十数行のコードで巨大なbotoを使わずに済みました。

蛇足ですが、
ファイル名を変更する必要がない場合は全てのresponse-content-dispositionのパラメータを外せばOKです。
response-content-dispositionを外さず元のファイル名を指定しておけば済む話ですが(汗)