FlaskでファイルアップロードAPIを実装し REST Clientで叩いた話

# TL; DR

FlaskでファイルアップロードAPIを実装し、REST Clientで叩きました。

 - [ソースコード(Github)](https://github.com/tesstesstex/API_TEST)

# はじめに
この記事ではFlaskで簡単なGETのAPIを実装するところから、
実際にファイルをPOSTするAPIを実装するところまでを、3 Stepsで記載しています。
Flaskにあまり馴染みが無い方も超簡単にファイルアップロードAPIできるようになれます（きっと）。

## 環境
Python 3.7
Flask 1.1
VSCode 1.38

>ディレクトリ構成
/
├ data/
├ \_\_init\_\_.py
├ api.py
├ call.http
├ face.png
└ requirements.txt

# 1. APIの実装(JSONのGET)

FlaskでのAPIの実装マジで簡単。やったぜ。

## 1-1. 実装

```python:api.py
from flask import Flask, jsonify

app = Flask(__name__)


@app.route('/api/item', methods=['GET'])
def get_item():
   item = {"item_name": 'hogehoge'}

   return jsonify(item) 

if __name__ == '__main__':
    app.run(debug=True)
```
これだけ。やったぜ。

## 1-2. 叩く

さっそく試しに叩いてみよう！
......あれよく考えたらあんまり叩き方知らないな？(情弱)

### 1-2-0. REST Client

ググってみたら次の記事を見つけました。

- [VS Code上でHTTPリクエストを送信し、VS Code上でレスポンスを確認できる「REST Client」拡張の紹介](https://qiita.com/toshi0607/items/c4440d3fbfa72eac840c)

>REST Client allows you to send HTTP request and view the response in Visual Studio Code directly.

VSCode上で HTTP リクエストができるとな？便利そう。
早速 [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) を入れて叩いてみました。

### 1-2-1. Flask起動

```sh
$ python api.py 
* Serving Flask app "api" (lazy loading)
* Environment: production
  WARNING: This is a development server. Do not use it in a production deployment.
  Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 111-858-152
```

### 1-2-2. リクエスト
```:call.http
### GET item
GET http://localhost:5000/api/item
```
Cmd + Option + R で送信！　（Windows は Ctl + Alt + R）

### 1-2-3. 結果

```http:response
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 30
Server: Werkzeug/0.16.0 Python/3.7.3
Date: Thu, 03 Oct 2019 16:54:12 GMT

{
  "item_name": "hogehoge"
}
```

やったぜ。

# 2. API 実装(ユーザー情報のPOST)
GET はうまくいったので POST も試してみよう。
ということでまずはFlaskでAPIを実装。

## 2-1. 実装
```python:api.py
from flask import Flask, jsonify, request
import bleach

app = Flask(__name__)

TOKEN = 'YOUR_TOKEN'

@app.route('/api/v1/user', methods=['POST'])
def add_user():
    # check token
    header = request.headers.get('Authorization', None)
    _, token = header.split()
    if token != TOKEN:
        return jsonify({'Forbidden': 'Access is denied'}), 403

    # method check
    if request.method != 'POST':
        return jsonify({'Method Not Allowed': 'Method is invalid.'}), 405

    # create new user
    new_user = {}
    for key in request.form.keys():
        new_user[key] = bleach.clean(request.form.get(key))

    return jsonify(new_user)


if __name__ == '__main__':
    app.run(debug=True)
```
これだけ。やったぜ。
ほんとはDBと繋いで new_user を登録したりしたんですけど、今回は割愛。

Flask でリクエストを受け取る時は **request** を使用します。
今回は Content-Type は multipart/form-data で送信するつもりなので、request.form を使用しています。
他にもパラメータを受け取る場合は request.args 、json を受け取る時は request.json を使用します。

## 2-2. 叩く

### 2-2-1. Flask起動
さっきと同じようにapi.pyを動かしておきましょう。

### 2-2-2. リクエスト
```:call.http
#######
### POST user info
POST http://localhost:5000/api/v1/user
Authorization: Bearer YOUR_TOKEN
Content-Type: multipart/form-data; boundary=HOGEHOGEBOUNDARY

--HOGEHOGEBOUNDARY
Content-Disposition: form-data; name=user_name

SAKAMOTO RYOMA
--HOGEHOGEBOUNDARY
Content-Disposition: form-data; name=user_email

ryoma.sakamoto@...
--HOGEHOGEBOUNDARY--
```
なるほど multipart 形式でPOSTする場合は boundary っていうのを設定するんですね。(情弱)
送信する要素ごとに **--{your boundary}** を記述して、
要素の最後は **--{your boundary}--** で締めくくるんですねぇ。 
このへんは[RFC7578](https://tools.ietf.org/html/rfc7578)で規定されていますのでご確認ください。

**Content-Disposition** の name が記述されていますが、これはHTMLの

```html:html
<form>
 <input name="user_name">
</form>
```
と同じです。

### 2-2-3. 結果
```http:response
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 98
Server: Werkzeug/0.16.0 Python/3.7.3
Date: Thu, 03 Oct 2019 17:24:57 GMT

{
  "user_email": "ryoma.sakamoto@...",
  "user_name": "SAKAMOTO RYOMA"
}
```
やったぜ。無事POSTできましたね。

# 3. APIの実装(アップロードファイルのPOST)
さて、ようやく本題です。

ユーザー情報送るなら顔写真とかも送れた方が良いよね？そうでもないですか？
ファイルアップロードのAPIって実装めんどくさいよなぁと思ってましたが超簡単でした！
そうFlaskとREST Clientならね。

## 3-1. 実装
(10月7日check token部分修正しました。)

```python:api.py
from flask import Flask, jsonify, request
import bleach

app = Flask(__name__)

TOKEN = 'YOUR_TOKEN'


@app.route('/api/v1/user', methods=['POST'])
def add_user():
    # check token
    header = request.headers.get('Authorization', None)
    if header is not None:
        _, token = header.split()
        if token != TOKEN:
            return jsonify({'Forbidden': 'Access is denied'}), 403
    else:
        return jsonify({'Forbidden': 'Access is denied'}), 403

    # method check
    if request.method != 'POST':
        return jsonify({'Method Not Allowed': 'Method is invalid.'}), 405

    # create new user
    new_user = {}
    for key in request.form.keys():
        new_user[key] = bleach.clean(request.form.get(key))

    ### このへん追加
    # save uploaded file into data folder
    for file in request.files:
        if file is None:
            break
        upload_file = request.files.get(file)
        upload_path = 'data/%s' % upload_file.filename
        upload_file.save(upload_path)
        new_user[file] = upload_file.filename

    return jsonify(new_user)


if __name__ == '__main__':
    app.run(debug=True)
```
これだけ。やったぜ。
Flaskでファイルを受け取る場合、 **request.files** の中に入っていきます。

## 3-2. 叩く
### 3-2-1. Flask起動
さっきと同じようにapi.pyを動かしておきましょう。
また、事前に顔写真 face.png を api.py と同じディレクトリに置いておきましょう。

### 3-2-2. リクエスト
```:call.http
#######
### OK
POST http://localhost:5000/api/v1/user
Authorization: Bearer YOUR_TOKEN
Content-Type: multipart/form-data; boundary=HOGEHOGEBOUNDARY

--HOGEHOGEBOUNDARY
Content-Disposition: form-data; name=user_name

SAKAMOTO RYOMA
--HOGEHOGEBOUNDARY
Content-Disposition: form-data; name=user_email

ryoma.sakamoto@...
--HOGEHOGEBOUNDARY
Content-Disposition: form-data; name="file"; filename="face.png"
Content-Type: application/octet-stream

< ./face.png
--HOGEHOGEBOUNDARY--
```

### 3-2-3. 結果
```http:response
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 98
Server: Werkzeug/0.16.0 Python/3.7.3
Date: Thu, 03 Oct 2019 18:01:26 GMT

{
  "file": "face.png",
  "user_email": "ryoma.sakamoto@...",
  "user_name": "SAKAMOTO RYOMA"
}
```
dataフォルダをのぞいてみるとちゃんと face.png が保存されていました！
<img alt="スクリーンショット 2019-10-04 3.03.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342105/33e79cd6-9b52-9a30-278c-c6dc3d83e1c8.png">
くぅ疲、これにて目的達成です。やったぜ。

# 感想
ほんとFlask はAPI さくっと書けて優秀だなぁと思います。
情弱の弁解ですが、普段はAPIはJavascriptのAjaxだったり、Pythonのrequestsだったりで叩いているのですが、
curlについて調べているときにREST Clientについて知ったので使ってみた次第です。

# 参考
### Qiita
 - [トークンを利用した認証・認可 API を実装するとき Authorization: Bearer ヘッダを使っていいのか調べた](https://qiita.com/uasi/items/cfb60588daa18c2ec6f5)
 
### Auth0
 - [Python と Flask で RESTful API を開発する](https://auth0.com/blog/jp-developing-restful-apis-with-python-and-flask/)