FastAPIにおけるファイルアップロードとダウンロードの色んな方法それぞれの纏め

# はじめに

FastAPIを使う場合ファイルのアップロードや提供をさせる時に色んな書き方がありますね。調べてみても書き方はそれぞれ違うものがあってどんな書き方をしたらいいか迷ってしまいます。だからこの記事では纏めてみたいと思います。

この記事の説明はFastAPIを含めたPythonがメインですが、全体の仕様をわかりやすいように、htmlとJavaScriptで書いたウェブページのコードもここに載せて一緒に説明します。又、基本的に動けばいいという機能重視なのでcssを使わず地味なページとなります。


# インストール

この記事は入門の記事ではないから書く必要ないかもしれませんが、一応必要なライブラリーのことも書いておきます。FastAPIを使う際にfastapiとuvicornは勿論ですが、フォームやファイルの扱いをしたい場合python-multipartもインストールしておく必要があります。
```
pip install fastapi uvicorn python-multipart
```


# 前提

今回のサンプルコードはFastAPIで書いたPythonコードを`web.py`に、htmlとJavaScriptで書いた静的ウェブページは`index.html`に保存しますので、2つのファイルから基本的に構成されます。
```
web.py
index.html
```

そしてこのコマンド実行します。
```
uvicorn web:app --reload
```

そうしたら`http://127.0.0.1:8000`でウェブアプリにアクセスします。


# ファイル提供させる方法

まずはファイルを提供する側から説明します。

## FileResponseを使う

FastAPIで準備しておいたファイルをダウンロードとして提供したい時色んな方法がありますが、一番簡単なのは`FileResponse`を使うことです。これは名前の通りファイルという形でレスポンスするためのクラスです。

例えば`xxx.txt`というファイル（名前は適当）があってダウンロードのために準備する場合はこう簡単に書けます。
```python:web.py
from fastapi import FastAPI,responses

app = FastAPI()

@app.get('/xxx')
async def download():
    return responses.FileResponse('xxx.txt',filename='fff.txt')
```

これで`http://127.0.0.1:8000/xxx`にアクセスしたらファイルダウンロードは始めて`fff.txt`という名前のファイルが保存されるはずです。

ここで`xxx.txt`はファイルのパスで、`fff.txt`ダウンロードされる時に保存される名前です。

ここに注意するべきは`filename=`はちゃんと書く必要があります。これがないとファイルダウンロードではなく、ただブラウザにそのファイルの内容を表示させるだけとなります。（この仕様はブラウザの設定にもよりますが）

## URLから任意のファイルをダウンロードさせる

任意の名前のファイルを指定してその中のファイルを読み込ませてダウンロードさせることもできます。

例えばこう書きます。
```python:web.py
from fastapi import FastAPI,responses

app = FastAPI()

@app.get('/download/{filename}')
async def download(filename: str):
    return responses.FileResponse(filename,filename=filename)
```

これで`http://127.0.0.1:8000/download/yyy.txt`にアクセスしたら`yyy.txt`ファイルをダウンロードできます。


## ページ内でダウンロードさせる

FastAPIだけの話ではないのですが、サーバーから提供されたファイルをページ移動せずにJavaScriptでダウンロードを始めさせる方法です。

まずPythonの方は静的ページを指定する部分を追加します。
```python:web.py
from fastapi import FastAPI,responses,staticfiles

app = FastAPI()

@app.get('/download/{filename}')
async def download(filename: str):
    return responses.FileResponse(filename)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

因みに`filename=`は今回入れていません。JavaScriptで仕様を制御するから、データだけ渡せばいいです。

htmlとJavaScriptの方は、fetchを使う場合。
```html:index.html
<button onclick="dload()">実行</button>
<script>
function dload() {
  fetch("/download/xxx.txt")
    .then((res) => res.blob())
    .then((blob) => {
      let a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = "fff.txt";
      a.click();
    });
}
</script>
```

わかりにくいから`async`と`await`で書き換えた方がいいかも。
```html:index.html
<button onclick="dload()">実行</button>
<script>
async function dload() {
  let blob = await (await fetch("/download/xxx.txt")).blob()
  let a = document.createElement("a");
  a.href = URL.createObjectURL(blob);
  a.download = "fff.txt";
  a.click();
}
</script>
```

代わりにaxiosを使う場合。
```html:index.html
<button onclick="dload()">実行</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
async function dload() {
  let res = await axios.get("/download/xxx.txt", { responseType: "blob" })
  let a = document.createElement("a");
  a.href = URL.createObjectURL(res.data);
  a.download = "fff.txt";
  a.click();
}
</script>
```

そうしたら`http://127.0.0.1:8000/`にアクセスするとこのように地味なボタンがある画面になります。

![jikkou_botan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/8e55aebf-a06d-2ea0-e4ff-bd6799f49997.png)

これを押したらダウンロードが始まります。


## Responseを使う

以上説明した`FileResponse`は予め保存しておいたファイルをそのまま渡す方法です。でもその他にも例えばファイルを別々で準備したり読み込んで少しの処理したりしたい場合は使えません。この場合は`Response`を使うという方法がいいです。

```python:web.py
from fastapi import FastAPI,Response

app = FastAPI()

@app.get('/download')
async def download():
    return Response('ああぁ',headers={'Content-Disposition': f'attachment; filename="aaa.txt"'})
```

これで`aaa.txt`という「ああぁ」しかないテキストファイルができます。

ここでヘッダーのところに`Content-Disposition`を入れてファイルとしてダウンロードさせることを示します。ファイルの名前もここに入れることになります。`FileResponse`の時`filename=`を入れるのと同じです。

又、ファイルを読み込んで`Response`に使うこともできます。例えばこう書いたら`FileResponse`を使う時と同じ結果になります。
```python:web.py
from fastapi import FastAPI,Response

app = FastAPI()

@app.get('/download/{filename}')
async def download(filename: str):
    with open(filename,'rb') as f:
        file = f.read()
    return Response(file,headers={'Content-Disposition': f'attachment; filename="{filename}"'})
```

これ例なら`FileResponse`を使った方が簡潔なので`Response`を使う必要ないかもしれません。

### ASCIIでない名前の場合

`Response`を使う時に一つ問題があります。実はこのままでは日本語などASCIIでない文字を名前にしたらエラーになります。

そうしないようにちょっと面倒ですが、このように文字を変換する必要があります。例えばこのように書き換えます。
```python:web.py
from fastapi import FastAPI,Response
from urllib.parse import quote

app = FastAPI()

@app.get('/download/{filename}')
async def download(filename: str):
    with open(filename,'rb') as f:
        file = f.read()
    hd = {'Content-Disposition': f"attachment; filename*=utf-8''{quote(filename)}"}
    return Response(file,headers=hd)
```


## StreamingResponseを使う

ストリームとしてダウンロードさせることもできます。この場合は`StreamingResponse`を使います。

使い方は色々ありますが、例えばこのようにファイルを読み込んで`yield from`でジェネレーターを作るのです。
```python:web.py
from fastapi import FastAPI,responses
from urllib.parse import quote

app = FastAPI()

@app.get('/download/{filename}')
async def download(filename: str):
    def iterstream():
        with open(filename,'rb') as f:
            yield from f
    hd = {'Content-Disposition': f"attachment; filename*=utf-8''{quote(filename)}"}
    return responses.StreamingResponse(iterstream(),headers=hd)
```

書き方はわかりにくいが、`Response`と`FileResponse`で書いた場合と同じようにファイルダウンロードできます。

尚、`yield from`を見て見慣れなくて意味わからないと感じる人も多いと思います。`iterstream()`関数は`from`を使わずにこうやって`for`ループで書き換えることもできます。
```python
def iterstream():
    with open(filename,'rb') as f:
        for st in f:
            yield st
```

用は、この書き方をするとファイルの中身は一気に読み込まれるのではなく、何度も繰り返して読み込まれることになるのです。


# アップロードを受け取る方法

次はブラウザからのファイルを受け取らせる方法です。

## Requestを使う方法

まずは一番基本的な方法から説明します。それは`Request`クラスとしてpostメソッドによって送られたデータを抽出することです。

例えばファイルを受け取ってすぐ保存するならこのように書きます。
```python:web.py
from fastapi import FastAPI,Request,responses,staticfiles

app = FastAPI()

@app.post('/upload')
async def upload(request: Request):
    form = await request.form()
    file = form['ufile']
    bf = await file.read()
    with open(file.filename,'wb') as f:
        f.write(bf)
    return responses.RedirectResponse('/',status_code=301)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

ファイルをアップロードするメインの`index.html`はとりあえずJavaScriptすら書かずにただhtmlのフォームを使ってもいいです。
```html:index.html
<form action="/upload" method="post" enctype="multipart/form-data">
  <input name="ufile" type="file"><br><br>
  <button type="submit">実行</button>
</form>
```

このようにファイル選択と実行するボタンしかないページができます。

![upload_botan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/caee599d-5e70-0c3e-bafd-2de5198092d4.png)

ここでファイルを選択して実行したらフォームとしてファイルがサーバーへ送られて処理されます。

送られたフォームはFastAPIで`Request`クラスのオブジェクトに含まれ、`await request.form()`で抽出できます。フォームの中で色々入れられますが、今回はファイルだけ。htmlで`name="ufile"`としたので、ここでは`form['ufile']`で抽出するということになります。


## formタグの代わりにJavaScriptで`FormData`を作る

上述htmlのformタグを使うという方法ではページ移動が行われますが、そうではなくJavaScriptで実行してデータだけ送ることもできます。そうしたら元のページに戻るための`RedirectResponse`も必要ないです。代わりに何かレスポンスを`return`こともできますが、特に何もなければ`return`なしでいいです。

FastAPIの方はちょっと書き直します。
```python:web.py
from fastapi import FastAPI,Request,staticfiles

app = FastAPI()

@app.post('/upload')
async def upload(request: Request):
    form = await request.form()
    file = form['ufile']
    bf = await file.read()
    with open(file.filename,'wb') as f:
        f.write(bf)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

`index.html`の方はこうやって`FormData`で書きます。
```html:index.html
<input id="ufile" type="file"><br><br>
<button onclick="upload()">実行</button>
<script>
function upload() {
  let inputelm = document.getElementById("ufile");
  let file = inputelm.files[0];
  if (file) {
    let formdata = new FormData();
    formdata.append("ufile", file);
    fetch("/upload", {
      method: "post",
      body: formdata,
    });
    inputelm.value = "";
  }
}
</script>
```

formタグがなくても見た目は変わりません。ファイル選択できて、実行をクリックしたらファイルが送られます。複雑になりますが、ページ移動せずにデータを送ることができます。


## UploadFileを使う

上述の方法よりも簡潔で、フォームのことを意識せずにすぐファイルを扱うことができます。それは`request: Request`を`ufile: UploadFile`に書き換えることです。
```python:web.py
from fastapi import FastAPI,UploadFile,staticfiles

app = FastAPI()

@app.post('/upload')
async def upload(ufile: UploadFile):
    bf = await ufile.read()
    with open(ufile.filename,'wb') as f:
        f.write(bf)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

`index.html`の方は上の例から変更ないので省略します。

`response`は`response`でこの名前でなければならないのに対し、ここでの`ufile`は固定の名前ではなく、フォームの中で指定した任意の名前です。今回の例では`formdata.append("ufile", file);`のところで決まったのです。

**型ヒント**で`UploadFile`を指定したことでFastAPIの中で自動的にフォームの中のファイルを抽出してくれるのです。そのおかげで書き方は簡潔で便利です。


## bytesとして受け取る

`UploadFile`を使う方法よりも更に簡潔な方法があります。それは`UploadFile`の代わりに`bytes = File()`を書くことです。そうしたら直接にバイトデータとして取得することになって、こうなると`await`を使う必要もなく、`async`も省略できます。

ただしこの方法ではファイルのデータしか取得できなくてファイルの名前などの情報が取得できないので、本当にファイルの中身しか必要としない場合だけ使った方がいいでしょう。例えば名前は既に決まっている場合です。

そうしたらこう書きます。
```python:web.py
from fastapi import FastAPI,File,staticfiles

app = FastAPI()

@app.post('/upload')
def upload(ufile: bytes = File()):
    with open('xxx.txt','wb') as f:
        f.write(ufile)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

又、その他に`Annotated`を使う書き方もありますが、結果は同じなので必要ありません。一応比べられるようにその書き方もここに載せておきます。
```python:web.py
from fastapi import FastAPI,File,staticfiles
from typing import Annotated

app = FastAPI()

@app.post('/upload')
def upload(ufile: Annotated[bytes,File()]):
    with open('xxx.txt','wb') as f:
        f.write(ufile)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

ファイルの名前を取得したいなら`request: Request`と一緒に使ってもいいです。

例えばこう書きます。
```python:web.py
from fastapi import FastAPI,Request,File,staticfiles

app = FastAPI()

@app.post('/upload')
async def upload(request: Request,ufile: bytes = File()):
    form = await request.form()
    with open(form['ufile'].filename,'wb') as f:
        f.write(ufile)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```


## データタイプやサイズを調べて対応する

ファイルと一緒に送られる情報はファイル名の他にも色々あります。例えばデータタイプやサイズです。これを調べてどう対応するか決めることもできます。

例えば`50kB`以内の`jpeg`か`png`しか受け取って、それ以外エラーを返すということもできます。
```python:web.py
from fastapi import FastAPI,UploadFile,File,staticfiles,HTTPException

app = FastAPI()

@app.post('/upload')
async def upload(ufile: UploadFile = File()):
    if(ufile.size>50*1024): # 50kB以内
        raise HTTPException(status_code=400, detail='太すぎる！')
    if(ufile.content_type not in ['image/jpeg','image/png']):
        raise HTTPException(status_code=400, detail='jpgとpngしか受け取らない')
    else:
        bf = await ufile.read()
        with open(ufile.filename,'wb') as f:
            f.write(bf)
        return 'アップロード完了'

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

`index.html`の方もエラーを表示するように書きます。
```html:index.html
<input id="ufile" type="file"><br><br>
<button onclick="upload()">実行</button>
<script>
function upload() {
  let inputelm = document.getElementById("ufile");
  let file = inputelm.files[0];
  if (file) {
    let formdata = new FormData();
    formdata.append("ufile", file);
    fetch("/upload", {
      method: "post",
      body: formdata,
    }).then(async (res) => {
      if (!res.ok)
        alert("エラー：" + (await res.json()).detail);
    });
    inputelm.value = "";
  }
}
</script>
```

尚、`content_type`属性は拡張子を元に自動的に追加されたが、中身は違う場合もあります。その場合他の方法で対策する必要がありますが、それについては別の話なので割愛します。


## 複数ファイルをアップロードさせる

単体の`UploadFile`の代わりにリストの`list[UploadFile]`に書き換えたら複数のファイルをリストとして受け取ることができます。

そして例えばこのように`for`ループで一つずつ保存することができます。
```python:web.py
from fastapi import FastAPI,UploadFile,staticfiles

app = FastAPI()

@app.post('/upload')
async def upload(lis_file: list[UploadFile]):
    for file in lis_file:
        bf = await file.read()
        with open(file.filename,'wb') as f:
            f.write(bf)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

`index.html`の方も複数ファイル選択できるように書き直します。
```html:index.html
<input id="lis_file" type="file" multiple><br><br>
<button onclick="upload()">実行</button>
<script>
function upload() {
  let inputelm = document.getElementById("lis_file");
  if (inputelm.files.length) {
    let formdata = new FormData();
    Array.from(inputelm.files).forEach((file) => {
      formdata.append("lis_file", file);
    });
    fetch("/upload", {
      method: "post",
      body: formdata,
    });
    inputelm.value = "";
  }
}
</script>
```

その他にも、`list[UploadFile]`を使わずに`request: Request`を使うこともできます。
```python:web.py
from fastapi import FastAPI,Request,staticfiles

app = FastAPI()

@app.post('/upload')
async def upload(request: Request):
    form = await request.form()
    for file_ in form._list:
        file = file_[1]
        bf = await file.read()
        with open(file.filename,'wb') as f:
            f.write(bf)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

`list[UploadFile]`を使った方が簡潔でわかりやすいですね。


## フォームを使わずにblobとして扱う

以上のやり方では全部フォームを通じてアップロードする方法ですが、実はフォームを使わずにデータだけそのまま送る方法もあります。ただしこの場合ファイル名などのデータはヘッダーに入れる必要があります。

使う例です。FastAPI側は`request: Request`で受け取って、`.body()`でファイルデータを抽出します。名前の方は`.headers['file-name']`から。
```python:web.py
from fastapi import FastAPI,Request,staticfiles

app = FastAPI()

@app.post('/upload')
async def upload(request: Request):
    bf = await request.body()
    name = request.headers['file-name']
    with open(name,'wb') as f:
        f.write(bf)

app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
```

`index.html`の方は`FormData`を作成せずに単に`input`タグの中のファイルをそのまま`fetch`に渡します。ただし`headers`では`"file-name"`属性にファイルの名前を入れます。
```html:index.html
<input id="ufile" type="file"><br><br>
<button onclick="upload()">実行</button>
<script>
function upload() {
  let inputelm = document.getElementById("ufile");
  let file = inputelm.files[0];
  if (file) {
    fetch("/upload", {
      method: "post",
      body: file,
      headers: { "file-name": file.name }
    });
    inputelm.value = "";
  }
}
</script>
```

尚、ここで`"file-name"`は任意の名前ではなく、固定です。`"filename"`などにしたら読み込めなくなります。ヘッダーの属性は何もかも勝手に入れられるわけではないからです。

この方法では一つしか送れないので、複数のファイルを送りたい場合やはりフォームに纏め送る必要があります。


# 参考＆もっと読む

- [[FastAPI]複数ファイルをrequests.post()で渡す](https://qiita.com/Akimoto_30s/items/a84a06111f6373b50dc3)
- [【リクエストとレスポンスを追いながら】丁寧に理解するFastAPI](https://www.geeklibrary.jp/counter-attack/fast-api/)
- [FastAPI】アップロード・ダウンロードファイルの操作](https://self-methods.com/fastapi-file/)
- [【Python】REST API でファイルをアップロードする (FastAPI)](https://qiita.com/XPT60/items/1ae2bb99e81bd8c8bc98)
- [JavaScriptのFetch API について](https://qiita.com/sotasato/items/31be24d6776f3232c0c0)
- [[JavaScript] ファイル選択ダイアログを表示してファイルを読み書きする](https://qiita.com/kerupani129/items/99fd7a768538fcd33420)
- [FastAPI でデータをダウンロードさせる](https://qiita.com/hoto17296/items/5624231b8d44b8f1275e)
- [【Python】FastAPIでファイルをダウンロードするWeb APIをつくる](https://cpp-learning.com/fastapi/)
- [FastAPIでファイルダウンロードさせる時のContent-Dispositionヘッダーの公開方法](https://qiita.com/kaihei777/items/681239c7d408e4ed8005)
- [FastAPIで画像をアップロードしてダウンロードする](https://www.chazine.com/archives/4116)
- [ファイルをレスポンスする【FastAPI】](https://liquids.dev/articles/edd4fa38-9c0f-4d79-a219-e0df5f4b00a7)
- [OpenCVで読み込んだ画像をFastAPIで返したいとき](https://qiita.com/chikiku/items/e73ae8ff281c48e7f361)
- [Python で手軽にWEB画面を作りたい](https://qiita.com/ydclab_P002/items/59639a72f5ffa07ada8d)
- [FastAPI を使って PC のファイルを別の場所にコピーするだけの無駄機能を作る](https://qiita.com/noritakaIzumi/items/8df2bfcb867fa99438b3)
- [fastAPI入門](https://qiita.com/mossan_hoshi/items/dea5742e46a979601140)
- [Pythonの型ヒント「Annotated」にDeepDiveしてみる](https://qiita.com/inetcpl/items/e9b8e5f9442d02175158)
- [Pythonとyieldとreturnと時々yield from](https://qiita.com/Daizoo/items/370901ae28c1f586b093)
- [Python Fast API ステータスコードやエラーハンドリングについてまとめてみた](https://qiita.com/nakapar_/items/74d8f2cc53bd5de6e51c)
- [FastAPIで実装した様々なエンドポイントのテストを書く（フォームデータの送信、クッキーの確認、ファイルのアップロード等）](https://qiita.com/kurumaebi65/items/d5cda239ef601f4c36ef)
- [FastApiで静的ファイルをマウントする](https://qiita.com/rubberduck/items/3734057d92a5ee7a2e83)
- [FastAPIを用いて音声ファイルをMongoDBに保存](https://qiita.com/n_yamadamadamada/items/76df5d1a42cf32e6c606)
- [FastAPI勉強メモ](https://qiita.com/qiuyin/items/ce6a3c31d13a5403ad67)
- [FastAPIでStarletteとPydanticはどのように使われているか](https://qiita.com/bee2/items/d629d8acc102cf92b7b2)
- [FastAPIとsqlite3による簡単なウェブサイトを実装する](https://qiita.com/phyblas/items/c3ff92b6dd353f887f1d)