FlaskでアップロードされたファイルのMIMEタイプを識別しようとしたら困った話

## 概要

Python Flaskで構築されたWebアプリケーションにおいて、アップロードされたファイルが想定しているContent-Typeのファイルなのかチェックしたい場合があります。
その際には、アップロードされたファイルのMIMEタイプを識別してバリデーションを行うことが一般的です。
Python Flaskで構築されたWebアプリにおいて、MIMEタイプを識別する主な方法は以下が挙げられます。

- Flaskのファイルアップロード
- Python標準ライブラリのmimetypes
- 外部ライブラリのpython-magic

本記事では、それぞれの懸念点や導入時に困ったことについて記載します。

## そもそもMIMEタイプとは

MIMEタイプとは、メールやWEBアプリのデータ転送の際にデータの種類を識別するためのコードです。
これらのコードは「タイプ名/サブタイプ名」の形式で表されます。

（参考）[MIMEタイプって何者？](https://qiita.com/Hamoto/items/dfff7e43e27887164aae)

## Flaskのファイルアップロード

Flaskでは、ファイルアップロード時に自動的にMIMEタイプを識別してくれる機能があります。
[Flaskアプリケーションのテスト](https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/testing.html#form-data)

しかし、環境によってはファイルの種類が不明のapplication/octet-streamとなることがありました。
これについては原因は不明ですが、不明なMIMEタイプだとバリデーションで弾かれるため注意が必要です。

## Python標準ライブラリのmimetypes

mimetypesはPythonの標準ライブラリのため、外部ライブラリに依存せずに実装できるので手軽です。
ただし、mimetypesはファイルの拡張子部分の文字列のみでMIMEタイプを識別するため、拡張子を意図的に変更したファイルなどは正しく検知できません。
そのため、ファイルの中身でMIMEタイプを識別したい場合はこの方法は不適切です。

## 外部ライブラリのpytyon-magic

python-magicはファイルの中身からMIMEタイプを識別することができるため、今回の要件には最も適していました。
今回はDockerコンテナ内でWebアプリケーションを実装したのですが、実際に動かしてみると以下のエラーが出力されてコンテナがrestartを繰り返してしまいました。

```Console
ImportError: failed to find libmagic. Check your installation
```

python-magicはlibmagicのラッパーなので、事前にOS側にlibmagicがインストールされている必要があるようです。

ちなみに`python:3.9.7-buster`にはデフォルトでインストールされているようですが、今回採用した`python:3.9.7-slim-buster`にはインストールされていませんでした。

Dockerfileにlibmagicのインストールコマンドを追記することで、python-magicを使用してアップロードされたファイルのMIMEタイプを正しく検知することができました。

### サンプルコード

以下はpython-magicの動作を確認するための簡易的なコードになります。

```text:./requirements.txt
Flask==2.1.0
python-magic==0.4.24
```

```Dockerfile:./Dockerfile
FROM python:3.9-alpine

WORKDIR /app

COPY requirements.txt ./
RUN apk add --no-cache file && \
    pip install --no-cache-dir -r requirements.txt
COPY app.py ./
COPY templates ./templates

EXPOSE 5000

CMD ["python", "app.py"]
```

```py:./app.py
from flask import Flask, render_template, request
import magic

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    # リクエストからファイルを取得する
    file = request.files['file']

    # ファイルタイプを判定する
    file_type = magic.from_buffer(file.read(), mime=True)

    # ログに出力する
    app.logger.info('File type: %s', file_type)

    return file_type

if __name__ == '__main__':
    app.run(host="0.0.0.0")
```

```html:./templates/index.html
<html>
  <body>
  	<form method="POST" action="/upload" enctype="multipart/form-data">
  	  <input type="file" name="file" />
  	  <input value="upload" type="submit"/>
  	</form>
  </body>
</html>
```

以下が上記のコードを実際に実行した際の結果になります。

下記コードを実行して、コンテナを起動してください。

```bash
docker build -t sample .
docker run -p 5000:5000 sample
```

ブラウザからhttp://(IPアドレス):5000にアクセスし、ファイルを選択して[upload]をクリックすると、選択したファイルのMIMEタイプが出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1826890/c339a18c-d1eb-4e21-b082-f5b5006fb6e5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1826890/9862f849-0ebc-d52d-7405-34c224d9ec79.png)


## まとめ

この記事は、Python FlaskでWebアプリケーションを構築し、アップロードされたファイルのMIMEタイプを識別してバリデーションする方法について述べました。
Flaskのファイルアップロード、Python標準ライブラリのmimetypes、外部ライブラリのpython-magicについて説明し、それぞれの懸念点や導入時に困ったことについて記載しています。