フロントエンドからファイルをmultipart/form-data形式でPOSTするためのWeb APIをサーバーレスで開発していて、データの取り出しに少しハマって調べたので書いておきます。
multipart/form-data
multipart/form-dataは、一回のPOSTリクエストで複数のデータやファイルをサーバーに送信するときに使うリクエストヘッダーです。
それぞれのファイルやデータをboundaryという文字列で区切ってリクエストボディに入れます。boundaryは多くのWebリクエストクライアントで自動的に生成してくれます。
curlを使ってmultipart/form-dataをリクエストする場合は下記のようになります。
curl -X POST http://localhost:3000 \
-F "data=@/path/to/sample.txt;type=text/plain" \
-F "csvdata=@/path/to/HelloWorld.csv;type=text/csv"
リクエストボディはこんな感じになります。
--------------------------5534b722b1776e54
Content-Disposition: form-data; name="data"; filename="sample.txt"
Content-Type: text/plain
This is Sample Text!!
--------------------------5534b722b1776e54
Content-Disposition: form-data; name="csvdata"; filename="HelloWorld.csv"
Content-Type: text/csv
foo,bar,baz
hoge,piyo,moge
--------------------------5534b722b1776e54--
boundaryは--------------------------5534b722b1776e54
の部分です。
ボディデータ内のどれがboundaryなのかをサーバーに教えて上げる必要があるのでヘッダーにその情報が入っています。
{'Content-Type': 'multipart/form-data; boundary=------------------------5534b722b1776e54'}
このヘッダー情報を使ってデータをパースしていく処理を自前で実装するのめちゃ大変・・・。
調べたところFieldStorageを使ってねということでした。
FieldStorage
FieldStorageはPython標準ライブラリに含まれるcgiモジュールのクラスです。FieldStorageにリクエストボディとヘッダー情報を引数として渡すと、下記の情報が取得できます。
- フィールド名(リクエスト時の
curl ... -F data=@hoge.txt
のdata
の部分) - ファイル名(例:
hoge.txt
) - ファイルタイプ(例:
text/plain
) - データ(例:
hello world
)
FieldStorageに渡す引数
fp
fpはfile pointer
のことで、ボディ本体を渡します。渡すボディはTextIOWrapper
クラスのオブジェクトである必要があるのでio.BytexIO()
で読み込みます。今回の場合はリクエストボディデータがBase64エンコードされた状態で送られてくるのでBase64デコード処理も入れています。
headers
content-typeとcontent-lengthを渡します。リクエストで受け取ったヘッダーはContent-Type
やContent-Length
になっていて、それをそのまま渡すとFieldStorageクラス内で読み込めないため、キーをlower caseにしてあげてます。(ここでめっちゃハマった・・)
environ
これはPOSTメソッドの操作だよと、REQUEST_METHODに入れてあげる必要があります。
コード
最終的にはこのような形になりました。 fs.list
をfor文で回して、それぞれのデータを取り出して処理しています。
import base64
import io
from cgi import FieldStorage
def parse_multipart_form(headers, body):
fp = io.BytesIO(base64.b64decode(body))
environ = {'REQUEST_METHOD': 'POST'}
headers = {
'content-type': headers['Content-Type'],
'content-length': headers['Content-Length']
}
fs = cgi.FieldStorage(fp=fp, environ=environ, headers=headers)
for f in fs.list:
print(f.name, f.filename, f.type, f.value)
出力
data sample.txt text/plain b'This is Sample Text!!\n'
csvdata HelloWorld.csv text/csv b'foo,bar,baz\nhoge,piyo,moge\n'
以上です。
今回はFieldStorageをこういう使い方をしている例があまり見つからず、cgiのコードを読み込みました。いい勉強になります。
本当は、せっかくサーバーレスのようなクラウドネイティブなアプリケーションを構築しているのであれば、フロントからのファイルアップロードは、S3に直接アップロードするほうがいろいろと楽ですよね。
最後に、
- API Gatewayを使うのであればバイナリサポートを有効にすること
- Lambdaファンクションに渡せるイベントデータの上限は6MB
この部分は原因の発見が難しいので要注意です。