大変なのかと思っていたのですが、ライブラリが揃っていて思っていたより簡単に実現できました。
環境
- 言語
- Python 3.5.2
- ライブラリ
- Flask 0.11
- boto3 1.3.1
AWS 側の設定として、 Web サーバから 対象となる S3 のファイルへ読み込みアクセスの許可が必要な場合があります。
コード
(略)
from boto3 import resource
from flask import send_file
from os
from zipfile import ZipFile
(中略)
def create_zip(filenames: list) -> str:
'''
受け取ったファイル名リストのファイルから成るZipを作成し、そのファイルパスを返す
:param filenames: S3からダウンロードするファイル名のリスト
:return: 作成された Zip ファイルの PATH
'''
bucket = 'BUKET_NAME_HERE' # S3 のbucket名をここに
key_prefix = 'KEY_PREFIX_HERE' # ファイルが置かれている場所が bucket 直下でなければ key をここに
tmp_dir = os.path.join(current_app.root_path, 'tmp') # 画像を一時保存するディレクトリを指定
s3 = resource('s3')
result_file_path = os.path.join(tmp_dir, 'result.zip')
with ZipFile(result_file_path, 'w') as result_zip:
for name in filenames:
try:
filename = os.path.join(tmp_dir, name) # 保存先
target_key = key_prefix + str(answer_id) + '/' + name # ダウンロード元
s3.Bucket(bucket).download_file(target_key, filename)
result_zip.write(filename=filename, arcname=name)
except Exception as e:
# エラー時の処理
return result_file_path
@app.route('/download/<int: file_id>')
def download_by_id(file_id: int):
files = get_file_names(id=file_id) # ID を元にファイル名の list を取得
zip_filename = create_zip(image_names=images, survey_id=survey_id)
return send_file(filename_or_fp=zip_filename, as_attachment=True)
解説
コントローラー
from flask import send_file
@app.route('/download/<int: file_id>')
def download_by_id(file_id: int):
files = get_file_names(id=file_id) # ID を元にファイル名の list を取得
zip_filename = create_zip(image_names=images, survey_id=survey_id)
return send_file(filename_or_fp=zip_filename, as_attachment=True)
/downlaod/1 のように ID を指定してアクセスすると、ファイルがダウンロードされます。
Flask には send_file という API があり、 サーバ内のファイルへの PATH を渡すと、クライアントへダウンロードさせるレスポンスを簡単に返すことが出来ます。 mimetype などの設定も Flask が自動判断してくれるので、凝ったことしない限り便利に使えそうです。
一点ハマったこととして、ファイルをダウンロードさせたければ send_file のオプションで、
as_attachment=True と指定しなければいけません。
このオプションはレスポンスヘッダに Content-Disposition: attachment と追加し、
添付ファイルであると明示的に指定します。
これがないと、ブラウザでファイルの中身が表示されてしまったり、
ダウンロードファイル名が変になってしまったりします。
モデル
from boto3 import resource
from zipfile import ZipFile
def create_zip(filenames: list) -> str:
'''
受け取ったファイル名リストのファイルから成るZipを作成し、そのファイルパスを返す
:param filenames: S3からダウンロードするファイル名のリスト
:return: 作成された Zip ファイルの PATH
'''
bucket = 'BUKET_NAME_HERE' # S3 のbucket名をここに
key_prefix = 'KEY_PREFIX_HERE' # ファイルが置かれている場所が bucket 直下でなければ key をここに
tmp_dir = os.path.join(current_app.root_path, 'tmp') # 画像を一時保存するディレクトリを指定
s3 = resource('s3')
result_file_path = os.path.join(tmp_dir, 'result.zip')
with ZipFile(result_file_path, 'w') as result_zip:
for name in filenames:
try:
filename = os.path.join(tmp_dir, name) # 保存先
target_key = key_prefix + + name # ダウンロード元
s3.Bucket(bucket).download_file(target_key, filename)
result_zip.write(filename=filename, arcname=name)
except Exception as e:
# エラー時の処理
return result_file_path
ここでは2つの処理を行っています。
- ファイルをS3から取得する
- 取得したファイルをZipアーカイブに入れる
ファイルをS3から取得する
S3 からの取得には Python 用 SDK boto3 を使用しています。
s3 = resource('s3')
resource というのは、公式によると
Boto 3 consists of the following major features: Resources: a high level, object oriented interface Collections: a tool to iterate and manipulate groups of resources Clients: low level service connections Paginators: automatic paging of responses Waiters: a way to block until a certain state has been reached
とのことで、 S3に関しての高度な操作まで揃ったインターフェイスを返してくれる…みたいです。
s3.Bucket(bucket).download_file(target_key, filename)
そして、リソース
s3 中の download_file メソッドを呼び出してローカルにS3のファイルをダウンロードします。
第二引数で保存先のファイル名を指定出来ます。
ここで一定の規則に従ったファイルにしておけば、後でまとめて消すときに楽です。
取得したファイルをZipアーカイブに入れる
Python 標準ライブラリーの zipfileを使用しています。 (あんまり使われてないのか、まだドキュメントの翻訳が完了しておらず、一部が英語のままです)
with ZipFile(result_file_path, 'w') as result_zip:
これで Zip ファイルを作成し、同時に読み込みモード(w) で開いています。
result_zip という名前で作成した Zip ファイルにアクセスできるようになります。
本来は close を明示的に書く必要がありますが、 with を使っていれば句の終わりで自動的に close してくれるのでこちらを使いましょう。
result_zip.write(filename=filename, arcname=name)
先ほど開いた zip ファイルに S3 から取得したファイルを書き込みます。
filename に書き込むファイルのフルパスを、arcname に zip ファイルで実際に書き込まれる時の名前を指定します。
このとき、arcname を指定しないと、フルパスのディレクトリ構造そのままでZipファイルが作成されてしまいます。注意しましょう。
江藤 光
まだまだ気持ちは新人です。