AI に関する世間の関心が高くなる中、アプリケーションを作成しようとすると必然的に機械学習のライブラリが豊富なPython が選択肢になり、Python でアプリを作るのであればそのフレームワークとしてFastAPI が有力な選択肢となります。本記事ではFastAPI について、簡単なアプリケーションを実装しながら学んでいきましょう。
目次
FastAPI とは
FastAPI は、Pythonで記述されたWeb API を構築するためのWebフレームワークです。シンプルで使いやすく、パフォーマンスに優れていながら、API ドキュメントの自動生成、セキュリティと認証の統合、依存性注入といった開発者にとって重要な機能もサポートしており、Python でAPI サーバーを作るうえでは非常に強力なフレームワークと言えます。
早速使うための準備をしていきましょう。
セットアップ
Python のインストールが済んでいる前提で解説を進めます。なお、FastAPI はPython 3.8 以上で動作します。
https://fastapi.tiangolo.com/#requirements
FastAPI を使うにあたって、合わせてUvicorn (またはHypercorn)もインストールする必要があります。インストール時には必要に応じてvenv などを有効にしてください。
pip install fastapi
pip install "uvicorn[standard]"
Uvicorn とはASGI (Asynchronous Server Gateway Interface)サーバーであり、一言でいえばFastAPI で作ったアプリケーションを実行するために必要なコンポーネントです。
ここで、少し寄り道してUvicorn のオプションを見てみましょう。
(venv) fastapi> uvicorn --help
Usage: uvicorn [OPTIONS] APP
Options:
--host TEXT Bind socket to this host. [default:
127.0.0.1]
--port INTEGER Bind socket to this port. If 0, an
available port will be picked. [default:
8000]
--uds TEXT Bind to a UNIX domain socket.
--fd INTEGER Bind to socket from this file descriptor.
--reload Enable auto-reload.
--reload-dir PATH Set reload directories explicitly, instead
of using the current working directory.
--reload-include TEXT Set glob patterns to include while watching
for files. Includes '*.py' by default;
these defaults can be overridden with
`--reload-exclude`. This option has no
effect unless watchfiles is installed.
--reload-exclude TEXT Set glob patterns to exclude while watching
for files. Includes '.*, .py[cod], .sw.*,
~*' by default; these defaults can be
overridden with `--reload-include`. This
option has no effect unless watchfiles is
installed.
--reload-delay FLOAT Delay between previous and next check if
application needs to be. Defaults to 0.25s.
[default: 0.25]
--workers INTEGER Number of worker processes. Defaults to the
$WEB_CONCURRENCY environment variable if
available, or 1. Not valid with --reload.
--loop [auto|asyncio|uvloop] Event loop implementation. [default: auto]
--http [auto|h11|httptools] HTTP protocol implementation. [default:
auto]
--ws [auto|none|websockets|wsproto]
WebSocket protocol implementation.
[default: auto]
--ws-max-size INTEGER WebSocket max size message in bytes
[default: 16777216]
--ws-max-queue INTEGER The maximum length of the WebSocket message
queue. [default: 32]
--ws-ping-interval FLOAT WebSocket ping interval in seconds.
[default: 20.0]
--ws-ping-timeout FLOAT WebSocket ping timeout in seconds.
[default: 20.0]
--ws-per-message-deflate BOOLEAN
WebSocket per-message-deflate compression
[default: True]
--lifespan [auto|on|off] Lifespan implementation. [default: auto]
--interface [auto|asgi3|asgi2|wsgi]
Select ASGI3, ASGI2, or WSGI as the
application interface. [default: auto]
--env-file PATH Environment configuration file.
--log-config PATH Logging configuration file. Supported
formats: .ini, .json, .yaml.
--log-level [critical|error|warning|info|debug|trace]
Log level. [default: info]
--access-log / --no-access-log Enable/Disable access log.
--use-colors / --no-use-colors Enable/Disable colorized logging.
--proxy-headers / --no-proxy-headers
Enable/Disable X-Forwarded-Proto,
X-Forwarded-For, X-Forwarded-Port to
populate remote address info.
--server-header / --no-server-header
Enable/Disable default Server header.
--date-header / --no-date-header
Enable/Disable default Date header.
--forwarded-allow-ips TEXT Comma separated list of IPs to trust with
proxy headers. Defaults to the
$FORWARDED_ALLOW_IPS environment variable
if available, or '127.0.0.1'.
--root-path TEXT Set the ASGI 'root_path' for applications
submounted below a given URL path.
--limit-concurrency INTEGER Maximum number of concurrent connections or
tasks to allow, before issuing HTTP 503
responses.
--backlog INTEGER Maximum number of connections to hold in
backlog
--limit-max-requests INTEGER Maximum number of requests to service
before terminating the process.
--timeout-keep-alive INTEGER Close Keep-Alive connections if no new data
is received within this timeout. [default:
5]
--timeout-graceful-shutdown INTEGER
Maximum number of seconds to wait for
graceful shutdown.
--ssl-keyfile TEXT SSL key file
--ssl-certfile TEXT SSL certificate file
--ssl-keyfile-password TEXT SSL keyfile password
--ssl-version INTEGER SSL version to use (see stdlib ssl
module's) [default: 17]
--ssl-cert-reqs INTEGER Whether client certificate is required (see
stdlib ssl module's) [default: 0]
--ssl-ca-certs TEXT CA certificates file
--ssl-ciphers TEXT Ciphers to use (see stdlib ssl module's)
[default: TLSv1]
--header TEXT Specify custom default HTTP response
headers as a Name:Value pair
--version Display the uvicorn version and exit.
--app-dir TEXT Look for APP in the specified directory, by
adding this to the PYTHONPATH. Defaults to
the current working directory.
--h11-max-incomplete-event-size INTEGER
For h11, the maximum number of bytes to
buffer of an incomplete event.
--factory Treat APP as an application factory, i.e. a
() -> <ASGI app> callable.
--help Show this message and exit.
非常にたくさんのオプションがありますが、例えば待ち受けるポート番号やSSL のバージョン、タイムアウト値など、FastAPI のようなアプリケーションのフレームワークとは切り離すことのできるサーバー側の実装をUnicorn は担っていると言えます。
早速簡単なアプリケーションを作って動作を試してみましょう。main.py を作成します。
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "Hello World"}
作成し保存したらuvicorn コマンドでサーバーを立ち上げます。
uvicorn main:app --reload
ブラウザを開いて、http://127.0.0.1:8000/ にアクセスしてみると、以下のようなJSON レスポンスが表示されるはずです。
{"message": "Hello World"}
これで、FastAPIを使用した最初の「Hello World」アプリケーション作成され、実行できました。
ルーティング
先ほどのHello World アプリケーションにおいて、FastAPI 独特の記法(デコレータ)として @app.get("/") が挙げられます。このようなデコレータをうまく使うことで、API のエンドポイントを制御していきます。
例えば先ほどのアプリにおいて、@app.get("/") から@app.get("/hello") に変えてみましょう。すると、http://127.0.0.1:8000/ ではNot Found が返され、http://127.0.0.1:8000/hello でないとアクセスできないことが確認できます。
同様に、POST やDELETE などのHTTP メソッドについてもデコレータで制御できます。以下のような簡単なタスク管理アプリケーションを実装してみましょう。先ほどのmain.py を以下に変更します。
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from uuid import uuid4, UUID
app = FastAPI()
class Task(BaseModel):
id: Optional[UUID] = None
description: str
completed: bool = False
tasks = []
# タスク一覧の取得
@app.get("/tasks/", response_model=List[Task])
async def read_tasks():
return tasks
# 新しいタスクの追加
@app.post("/tasks/", response_model=Task)
async def create_task(task: Task):
task.id = uuid4()
tasks.append(task)
return task
# タスクの削除
@app.delete("/tasks/{task_id}", response_model=Task)
async def delete_task(task_id: UUID):
for task in tasks:
if task.id == task_id:
tasks.remove(task)
return task
raise HTTPException(status_code=404, detail="Task not found")
同様に実行します。
uvicorn main:app --reload
http://127.0.0.1:8000/tasks にアクセスしても、[] と空のJSON が返ってきます。これはタスクがまだ何もないからです。そこで、POST リクエストを送ってタスクの追加を試します。curl してもいいのですが、せっかくなので自動生成されるAPI ドキュメントから実行してみましょう。http://127.0.0.1:8000/docs にアクセスしてみます。
このAPI ドキュメントからAPI リクエストを実行することができます。
POST リクエストのBody には、アプリケーションで定義したTask クラスのid などのフィールドがあらかじめ定義されています。UUID も自動で生成されているので、ここではdescription をwashing に変更してExecute してみます。
コード200 が返ってきているので、期待通りにタスクが追加されているはずです。GET もドキュメントから試してみましょう。
追加したwashing タスクが取得できていることが確認できます。さらなるタスクの追加や、タスクの削除などもぜひ試してみてください。DELETE 時にはあえて異なるUUID を入力し、404 エラーが返ってくることも試してみると面白いかもしれません。
リクエストとレスポンスの基本
API とは、いうなればどのようなリクエストがクライアントから送信され、どのようなレスポンスをサーバーが返すか、というシステムです。先ほどのタスク管理アプリを例に、もう少しこの点を深堀してみましょう。
以下のPOST リクエストのコードに注目します。
@app.post("/tasks/", response_model=Task)
async def create_task(task: Task):
task.id = uuid4()
tasks.append(task)
return task
ここで、create_task 関数の引数であるtask はコード中に定義された以下のTask クラス型を持ちます。
class Task(BaseModel):
id: Optional[UUID] = None
description: str
completed: bool = False
つまり、POST リクエストを送信する際は、このようなid やdescription、comleted といったフィールドを持たなければならず、かつuuid、str、bool などの型をそれぞれ持たなければなりません。このような型から外れてしまうと、以下のようにPOST リクエストは失敗します。
このようなバリデーション機能により、API サーバーに対して一貫した方法で安全にデータを渡すことができます。このバリデーションはimport しているPydantic によるもので、FastAPI の重要なコンポーネントです。せっかくなので、もう少しPydantic のBaseModel について説明します。BaseModel により、バリデーションやデータの整形を行うことができます。
以下のコードを別のファイルで作成し、実行してみましょう(Uvicorn は使わず、普通にpython ファイル名 で実行)。
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
user = User(name="test", age=20)
# データのJSON への整形
print(user.json())
このようなコードで、たとえばname をダブルクォートで囲まず数字を入れたときはFastAPI の時と同様にエラーが発生します。
さて、ここでUser クラスはBaseModel クラスを継承しています。継承しているので、BaseModel のコンストラクタである__init__ メソッドを持ち、__init__ で定義されている self.pydantic_validator.validate_python でバリデーションエラーが発生します。
def __init__(self, /, **data: Any) -> None: # type: ignore
"""Create a new model by parsing and validating input data from keyword arguments.
Raises [`ValidationError`][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
`self` is explicitly positional-only to allow `self` as a field name.
"""
# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
__tracebackhide__ = True
self.__pydantic_validator__.validate_python(data, self_instance=self)
https://github.com/pydantic/pydantic/blob/d77a9403603cfc125b9ff14ea9a45ae15f86b6ed/pydantic/main.py
内部的にはRust で書かれたpydantic-core のSchemaValidator を呼び出しています。例えばstring のチェックでは以下のソースコードが該当しますが、検証には正規表現を使っていることが分かります。
話が脱線しましたが、要するにPydantic のBaseModel を使って、クライアントからのリクエストを検証しているわけです。
レスポンスはどうでしょうか?response_model=Task とあるように、これもPydantic による検証が行われます。ここではリクエストとレスポンスは同じ型ですが、変えることもできます。詳細は下記ドキュメントを参照してください。
https://fastapi.tiangolo.com/tutorial/response-model
リクエストとレスポンスの応用
フロントエンドとの連携を意識して、FastAPI をもう少し様々な形式のリクエストやレスポンスに対応するように調整してみましょう。ここでは、フォームとファイルアップロードについて説明します。
フォーム
フォーム(Form)とはその名の通りHTML Formから送信されたデータをここでは指します。典型的にはテキストボックスに入力された値、例えばサイトの問い合わせ欄にあるテキストボックスに質問を記載して送信ボタンを押すと、通常フォームとしてデータがクライアントからWeb サーバーに送信されます。つまり、一般的にはフォームとしてPOST リクエストがクライアントから送信されるわけです。先ほどまではAPI ドキュメントからJSON 形式で送信しましたが、実際にフロントエンドまで含めたアプリケーションを作る場合は、フォームとしてまずデータを受け取り、その後JSON に整形し、FastAPI などのサーバーに送信するほかに、フォームとしてそのまま送信することもできます。
ここで、フォームデータをPOST で送るとき、送信されるデータの形式を示すヘッダのContent-Type にはapplication/x-www-form-urlencoded または multipart/form-data が与えられます。前者はフォームの一般的な値、後者はファイル送信を含む場合の値です。JSON の場合はapplication/json ですので、フォームデータを送信する場合とJSON を送信する場合にはHTTP リクエストのContent-Type が異なります。
先ほどのタスク管理アプリに戻りましょう。先ほどはJSON としてAPI ドキュメントからタスクデータを送信していましたが、今度はフォームとしてデータを送信できるように修正してみましょう。事前にpython-multipart をインストールしておきます。
pip install python-multipart
その後、タスク管理アプリを以下のように修正します。
from fastapi import FastAPI, HTTPException, Form
from pydantic import BaseModel
from typing import List, Optional
from uuid import uuid4, UUID
app = FastAPI()
class Task(BaseModel):
id: Optional[UUID] = None
description: str
completed: bool = False
tasks = []
# タスク一覧の取得
@app.get("/tasks/", response_model=int)
async def read_tasks():
return 1
# 新しいタスクの追加
@app.post("/tasks/", response_model=Task)
async def create_task(description: str = Form(...), completed: bool = Form(False)):
task = Task(id=uuid4(), description=description, completed=completed)
tasks.append(task)
return task
# タスクの削除
@app.delete("/tasks/{task_id}", response_model=Task)
async def delete_task(task_id: UUID):
for task in tasks:
if task.id == task_id:
tasks.remove(task)
return task
raise HTTPException(status_code=404, detail="Task not found")
これまでと同様にFastAPI を起動すると、API ドキュメントのRequest Body に設定するContent Type がapplication/json からapplication/x-www-form-urlencoded に変わっており、実際の送信形式や開発者ツールから見られるリクエストヘッダのContent Type も変更されていることが分かります。なお、レスポンスはJSON で返ってきています。
これで一般的なフォームデータをFastAPI に対して送信し、処理できることが分かりました。
ファイルアップロード
ファイルも実際にはフォームデータとしてアップロードされます。つまり、リクエストのContent Type がmultipart/form-data となります。application/x-www-form-urlencoded とmultipart/form-data はエンコードの方法が異なり、前者はアルファベット以外の文字がパーセントエンコーディング されていることから、バイナリデータを含むような送信には向いていません。なお、パーセントエンコーディングについては、先ほどのフォームデータ送信の際に日本語を入力すればどのようにエンコードされているかが分かります。下記スクリーンショットはdescription に「あ」を入れた場合です。「あ」という文字が%E3%81%82 にエンコードされていますね。
またまた話がそれましたが、multipart/form-data というContent Type を指定することで、データのこのようなエンコーディングを防ぎ、ブロックデータとして直接送信することができます。試してみましょう。
from fastapi import FastAPI, HTTPException, Form, File, UploadFile
from pydantic import BaseModel
from typing import List, Optional
from uuid import uuid4, UUID
app = FastAPI()
class Task(BaseModel):
id: Optional[UUID] = None
description: str
completed: bool = False
tasks = []
# タスク一覧の取得
@app.get("/tasks/", response_model=int)
async def read_tasks():
return 1
# 新しいタスクの追加
@app.post("/tasks/", response_model=Task)
async def create_task(description: str = Form(...), completed: bool = Form(False)):
task = Task(id=uuid4(), description=description, completed=completed)
tasks.append(task)
return task
# タスクの削除
@app.delete("/tasks/{task_id}", response_model=Task)
async def delete_task(task_id: UUID):
for task in tasks:
if task.id == task_id:
tasks.remove(task)
return task
raise HTTPException(status_code=404, detail="Task not found")
@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
return {"filename": file.filename}
実行すると、API ドキュメントからファイルをアップロードできるようになっているはずです。
ちなみに、今回2つのAPI を追加しています。つまり、入力の形式として、Annotated[bytes, File()] のパターンとUploadFile のパターンです。この2つの違いをもう少し詳しく見てみましょう。
前者のAnnotated[bytes, File()] としてファイルを受け取る場合、バイナリデータとしてファイルを受信し、アップロードされたコンテンツはすべてメモリ上に保存されます。バイナリデータとして受け取るわけですから、ファイル名などの取得もできません。UploadFile で受け取る場合は、メタデータの取得も簡単に行えたり、巨大なサイズのファイルでもメモリ制限に引っかかることがなかったりと、より柔軟な形式と言えます。下記コードは、受け取ったファイルをmain.py と同じディレクトリに保存しますが、Annotated[bytes, File()] はファイル名がハードコードされているのに対し、UploadFile ではアップロードされたファイル名を取得して保存しています。
import os # 追加
@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
# main.py と同じディレクトリにファイルをfile.txt という名前で保存
path = os.path.join("./", "file.txt")
with open(path, "wb") as f:
f.write(file)
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
with open(file.filename, "wb") as f:
content = await file.read()
f.write(content)
return {"filename": file.filename}
UploadFile の方が何かと扱いやすいので、特に理由がなければ、ファイルはUploadFile として受け取った方が良いと思います。
エラーハンドリング
最後に、エラーハンドリングについて簡単に学びます。先ほどまでのアップロードシステムはセキュリティ的に非常に脆弱です。そのままだとあらゆるファイルのアップロードを許可することになりますので、エラーハンドリングを行うことでセキュリティを強化してみましょう。
まずは、画像以外のアップロードを許可しないようにしてみます。create_upload_file を以下のように修正します。
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
# 画像ファイルのMIMEタイプを許可するリスト
allowed_mime_types = ["image/jpeg", "image/png"]
# ファイルが許可されたMIMEタイプの1つであることを確認
if file.content_type not in allowed_mime_types:
raise HTTPException(status_code=400, detail=f"File type not allowed. Allowed types: {', '.join(allowed_mime_types)}")
return {"filename": file.filename, "content_type": file.content_type, "file_size": file.size}
ここで、MIME タイプとは簡単に言うとアップロードされたファイルがどのようなデータの種類か、を示します。これはリクエストのContent-Type ヘッダによって決められます。multipart/form-data とは別に、例えばjpg をアップロードする場合には、type=image/jpeg といったヘッダも付くため、これでもってファイル形式を判断できます。
リクエストのContent-Type を見るということは、実はいくらでも偽装できる余地があるということです。実際、例えばAPI ドキュメントからWindows のexe ファイルの拡張子をjpg にした場合でも、実はMIME はimage/jpeg とみなされ、エラーは発生しません。手元にあったWireshark の実行ファイルの拡張子をjpg に変更して試してみると下記のようにjpeg とみなされてしまっていることが分かります。
これを防ぐために、今回はpython-magic というライブラリを活用してマジックナンバーを用いたファイル種判別も組み合わせてみます。マジックナンバーとはファイル先頭のファイルタイプに共通するデータを指します。例えばJPEG は通常FF D8 FF E0 から始まりますので、これをもとにファイルを判定します。ほかのファイルについては例えば下記を参照してください。
https://en.wikipedia.org/wiki/List_of_file_signatures
python-magic はpip でインストールできますが、libmagic に依存するため、こちらのインストールも忘れないでください。例えばWindows の場合は以下の通りです。
pip install python-magic-bin
pip install python-magic
import magic # 追加
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
# 画像ファイルのMIMEタイプを許可するリスト
allowed_mime_types = ["image/jpeg", "image/png"]
buffer = await file.read()
magic_mime_type = magic.from_buffer(buffer, True)
await file.seek(0)
# ファイルが許可されたMIMEタイプの1つであることを確認
if file.content_type not in allowed_mime_types or magic_mime_type not in allowed_mime_types:
raise HTTPException(status_code=400, detail=f"File type not allowed. Allowed types: {', '.join(allowed_mime_types)}")
return {"filename": file.filename, "content_type": file.content_type, "file_size": file.size}
これで、拡張子が偽装されていた場合でもエラーが正しく発生するはずです。
ではマジックナンバーがHEX エディタ等で偽装されていた場合はどうでしょうか?……いたちごっこ感が強いですね。
このようなファイルの安全性の検証についてはいろいろな手法があり、限度もあるわけですが、最近登場した面白い方法として、AI をもとにファイル種を判別するgoogle のmagika があります。軽量でアプリケーションに組み込みやすく、精度も99%を達成したということで、ファイル種判別にはかなり使えるのではないかと思います。ちなみにデモサイトで簡単に試せます。
from magika import Magika # 追加
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
# 画像ファイルのMIMEタイプを許可するリスト
allowed_mime_types = ["image/jpeg", "image/png"]
buffer = await file.read()
magic_mime_type = magic.from_buffer(buffer, True)
m = Magika()
magika_mime_type = m.identify_bytes(buffer).output.mime_type
await file.seek(0)
print(f"file.content_type: {file.content_type}, magic_mime_type: {magic_mime_type}, magika_mime_type: {magika_mime_type}")
# ファイルが許可されたMIMEタイプの1つであることを確認
if not (file.content_type in allowed_mime_types and
magic_mime_type in allowed_mime_types and
magika_mime_type in allowed_mime_types):
raise HTTPException(status_code=400, detail=f"File type not allowed. Allowed types: {', '.join(allowed_mime_types)}")
return {"filename": file.filename, "content_type": file.content_type, "file_size": file.size}
なお、拡張子を偽装したファイルがどう見えるかログに吐いてみましたが、マジックナンバーとmagika 両方で実行ファイルであることが分かります。
file.content_type: image/jpeg, magic_mime_type: application/x-dosexec, magika_mime_type: application/x-dosexec
ここまで画像ファイルのみを許可するAPI を作成してきましたが、ファイルサイズの判定も重要です。ついでに、巨大な画像をアップロードされないようにエラーハンドリングを追加しましょう。10MB 以上の画像のアップロードに対してエラーを出してみます。
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
# 画像ファイルサイズの上限を設定
if file.size > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="File size too large. Max 10MB allowed.")
# 画像ファイルのMIMEタイプを許可するリスト
allowed_mime_types = ["image/jpeg", "image/png"]
buffer = await file.read()
magic_mime_type = magic.from_buffer(buffer, True)
m = Magika()
magika_mime_type = m.identify_bytes(buffer).output.mime_type
await file.seek(0)
print(f"file.content_type: {file.content_type}, magic_mime_type: {magic_mime_type}, magika_mime_type: {magika_mime_type}")
# ファイルが許可されたMIMEタイプの1つであることを確認
if not (file.content_type in allowed_mime_types and
magic_mime_type in allowed_mime_types and
magika_mime_type in allowed_mime_types):
raise HTTPException(status_code=400, detail=f"File type not allowed. Allowed types: {', '.join(allowed_mime_types)}")
return {"filename": file.filename, "content_type": file.content_type, "file_size": file.size}
このように、様々な例外判定とそれに対応するメッセージを作成することで、堅牢で使いやすいAPI を作成できます。
最後に一点補足すると、通常、API ドキュメントから直接ファイルをアップロードすることはなく、フロントエンドのアプリケーションからFastAPI にデータが送信されます。つまり、このような不適切なファイルの検証は当然フロントエンドで行い、そもそも送信されること自体を防ぐべきです。が、何らかの理由で送信されてしまった場合に何も対処をしていないと困りますので、API サーバー側でもこのようなエラーハンドリングは行うべきだと思います。ただ、完全な防御というものは存在しないので、不必要にアップロードされたファイルを保存しない、ましてそのまま実行しないなど、仮に悪意のあるファイルがアップロードされたときのことも考えた、リスク低減を加味したアプリケーションの設計をすべきです。
最後に、最終的に出来上がったアプリケーションのソースコードを共有します。
from fastapi import FastAPI, HTTPException, Form, File, UploadFile
from pydantic import BaseModel
from typing import List, Optional
from uuid import uuid4, UUID
from typing import Annotated
import magic
import os
from magika import Magika
app = FastAPI()
class Task(BaseModel):
id: Optional[UUID] = None
description: str
completed: bool = False
tasks = []
# タスク一覧の取得
@app.get("/tasks/", response_model=int)
async def read_tasks():
return 1
# 新しいタスクの追加
@app.post("/tasks/", response_model=Task)
async def create_task(description: str = Form(...), completed: bool = Form(False)):
task = Task(id=uuid4(), description=description, completed=completed)
tasks.append(task)
return task
# タスクの削除
@app.delete("/tasks/{task_id}", response_model=Task)
async def delete_task(task_id: UUID):
for task in tasks:
if task.id == task_id:
tasks.remove(task)
return task
raise HTTPException(status_code=404, detail="Task not found")
@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
# main.py と同じディレクトリにファイルを保存
path = os.path.join("./", "file.txt")
with open(path, "wb") as f:
f.write(file)
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
# 画像ファイルサイズの上限を設定
if file.size > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="File size too large. Max 10MB allowed.")
# 画像ファイルのMIMEタイプを許可するリスト
allowed_mime_types = ["image/jpeg", "image/png"]
buffer = await file.read()
magic_mime_type = magic.from_buffer(buffer, True)
m = Magika()
magika_mime_type = m.identify_bytes(buffer).output.mime_type
await file.seek(0)
print(f"file.content_type: {file.content_type}, magic_mime_type: {magic_mime_type}, magika_mime_type: {magika_mime_type}")
# ファイルが許可されたMIMEタイプの1つであることを確認
if not (file.content_type in allowed_mime_types and
magic_mime_type in allowed_mime_types and
magika_mime_type in allowed_mime_types):
raise HTTPException(status_code=400, detail=f"File type not allowed. Allowed types: {', '.join(allowed_mime_types)}")
return {"filename": file.filename, "content_type": file.content_type, "file_size": file.size}
まとめ
本記事では、簡単なアプリケーションの作成を通して、FastAPI の基本について学びました。FastAPI はドキュメントがチュートリアル付きでとても充実しているので、次のステップとしてはドキュメントを読みながら必要な機能を学習しつつ、実際にアプリケーションを作ってみるとよいかと思います。