本文 AI 產出,尚未審核

FastAPI – 表單與檔案上傳:UploadFile 屬性深入解析


簡介

在現代 Web 應用中,檔案上傳是最常見的需求之一,無論是使用者上傳頭像、簡報、或是批次匯入資料,都離不開對檔案的接收與處理。FastAPI 以其非同步設計與型別提示(type‑hint)支援,讓檔案上傳變得既簡潔又高效。

本單元聚焦於 UploadFile 這個核心類別,特別說明它的三個常用屬性:filenamecontent_typefile.read()。了解這些屬性的意義與正確使用方式,能讓開發者在 表單 + 檔案 的 API 中,快速完成驗證、儲存與後續處理,避免常見的效能與安全問題。


核心概念

1. 為什麼使用 UploadFile 而不是 bytes

FastAPI 允許兩種方式接收檔案:

方式 型別 儲存方式 典型使用情境
bytes bytes 直接讀入記憶體 檔案非常小(如 < 1 KB)
UploadFile UploadFile 暫存於磁碟(或系統暫存) 大檔案、需要非同步讀寫、或要重複讀取

使用 UploadFile 可以 避免一次性將整個檔案載入記憶體,提升效能與穩定性,尤其在上傳圖片、影片或 CSV 等較大的檔案時格外重要。

2. UploadFile 的三大屬性

屬性 型別 目的 範例
filename str 原始檔名(含副檔名) "avatar.png"
content_type str MIME 類型(如 image/png "image/png"
file SpooledTemporaryFile(類似檔案物件) 透過 read()、write()、seek() 操作實體檔案 await upload_file.read()

⚡ 小技巧content_type 只是一個 聲明,若需嚴格驗證,仍要自行檢查檔案內容。

3. 非同步讀取檔案內容

UploadFile.file 本身是一個 非同步 的檔案物件,提供 await file.read()await file.write() 等方法。以下範例示範如何在不阻塞事件迴圈的情況下,讀取全部位元組或分塊讀取。

程式碼範例 1:基礎檔案上傳與屬性取得

from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/upload/basic")
async def upload_basic(file: UploadFile = File(...)):
    # 取得檔名與 MIME 類型
    filename = file.filename
    mime = file.content_type

    # 讀取全部內容(小檔案適用)
    content = await file.read()

    return {
        "filename": filename,
        "content_type": mime,
        "size": len(content)  # 以位元組為單位
    }

說明

  • File(...) 表示此欄位為必填。
  • await file.read() 會一次性把檔案讀入記憶體,適合檔案小於 1 MB 左右。

程式碼範例 2:分塊讀取大型檔案(流式)

from fastapi import FastAPI, File, UploadFile
import aiofiles

app = FastAPI()

@app.post("/upload/stream")
async def upload_stream(file: UploadFile = File(...)):
    # 以非同步方式寫入磁碟
    async with aiofiles.open(f"tmp/{file.filename}", "wb") as out_file:
        while chunk := await file.read(1024 * 1024):   # 每次 1 MB
            await out_file.write(chunk)

    return {"detail": f"檔案已儲存至 tmp/{file.filename}"}

重點

  • 使用 while chunk := await file.read(size) 讓程式只在需要時才讀取資料,降低記憶體佔用
  • aiofiles 為非同步檔案 I/O 套件,配合 UploadFile 可完整保持非同步。

程式碼範例 3:驗證 MIME 類型與副檔名

from fastapi import FastAPI, File, UploadFile, HTTPException

app = FastAPI()

ALLOWED_TYPES = {
    "image/jpeg": ".jpg",
    "image/png": ".png",
    "application/pdf": ".pdf",
}

@app.post("/upload/validate")
async def upload_validate(file: UploadFile = File(...)):
    # 1️⃣ 檢查 MIME
    if file.content_type not in ALLOWED_TYPES:
        raise HTTPException(status_code=400, detail="不支援的檔案類型")

    # 2️⃣ 檢查副檔名是否相符(避免偽裝)
    expected_ext = ALLOWED_TYPES[file.content_type]
    if not file.filename.lower().endswith(expected_ext):
        raise HTTPException(status_code=400, detail="副檔名與 MIME 不符")

    # 3️⃣ 讀取前 512 位元組做簡易魔術數檢查(可自行擴充)
    head = await file.read(512)
    await file.seek(0)   # 讀完後必須回到開頭,否則後續寫入會失敗

    # 此處可加入更嚴格的檔案內容驗證

    # 暫存至磁碟
    async with aiofiles.open(f"uploads/{file.filename}", "wb") as f:
        await f.write(head)
        while chunk := await file.read(1024 * 1024):
            await f.write(chunk)

    return {"detail": "上傳成功"}

說明

  • await file.seek(0) 必須 在讀取過檔案後呼叫,否則後續的寫入會從檔案結尾開始。
  • 這個範例示範 MIME + 副檔名雙重驗證,降低惡意上傳的風險。

程式碼範例 4:同時接收多個檔案與一般欄位

from fastapi import FastAPI, File, Form, UploadFile
from typing import List

app = FastAPI()

@app.post("/upload/multi")
async def upload_multi(
    description: str = Form(...),
    files: List[UploadFile] = File(...)
):
    saved = []
    for f in files:
        async with aiofiles.open(f"multi/{f.filename}", "wb") as out:
            while chunk := await f.read(1024 * 1024):
                await out.write(chunk)
        saved.append(f.filename)

    return {"description": description, "saved_files": saved}

亮點

  • 使用 Form(...) 取得普通文字欄位。
  • List[UploadFile] 讓 API 能一次接收多個檔案,不需要額外迴圈在路由層

4. UploadFile 內建的 spoiled 行為

UploadFile 內部使用 SpooledTemporaryFile,當檔案大小低於系統預設的 max_size(預設 1 MB) 時,會暫存於記憶體;超過時自動寫入磁碟。這意味著:

  • 小檔案await file.read() 仍會一次性載入記憶體。
  • 大檔案:即使使用 await file.read(),FastAPI 也會在背後把檔案寫入磁碟,再一次性讀回,仍會佔用大量記憶體。

因此,對於超過 1 MB 的檔案,建議使用分塊讀取(如範例 2)以避免突發的記憶體尖峰。


常見陷阱與最佳實踐

陷阱 可能的後果 解決方式
直接使用 await file.read() 讀取大型檔案 記憶體 OOM(Out‑Of‑Memory) 改用 分塊讀取,或設定 max_length 限制檔案大小
忘記 await file.seek(0) 後續寫入或再次讀取時得到空內容 讀完檔案後立即回到檔案開頭
只依賴 content_type 進行驗證 攻擊者可偽造 MIME,導致惡意檔案上傳 同時檢查 副檔名魔術數(檔案前幾位元組)
未設定檔案大小上限 大量上傳可能導致服務資源耗盡 在路由或全局層面使用 max_lengthUploadFileFile(..., max_length=...)
同步寫檔 (open(..., "wb")) 阻塞事件迴圈,降低吞吐量 使用 aiofiles 或其他非同步 I/O 套件

推薦的最佳實踐

  1. 限制檔案大小File(..., max_length=5_242_880)(限制 5 MB)。
  2. 分塊處理:對於 > 1 MB 的檔案,使用 while chunk := await file.read(CHUNK_SIZE)
  3. 雙重驗證:MIME + 副檔名 + 必要時的魔術數檢查。
  4. 非同步 I/O:全程保持非同步,避免 open()write() 等阻塞。
  5. 清理暫存檔:若自行將檔案寫入磁碟,完成後適時刪除或搬移至永久儲存區。

實際應用場景

場景 為何需要 UploadFile 典型實作要點
使用者頭像上傳 圖片通常在 100 KB–2 MB,需即時回傳 URL 直接 await file.read(),儲存至雲端儲存(S3、Google Cloud Storage),回傳公開 URL
批次匯入 CSV/Excel 檔案大小可能達數十 MB,需要逐行解析 使用分塊讀取,搭配 csv.AsyncReaderpandas.read_csv(..., chunksize=…)
上傳 PDF 報表 需要驗證檔案類型並儲存於內部文件庫 先檢查 content_type、副檔名與前 4 位元組 %PDF,再寫入磁碟或雲端
影片或音訊檔上傳 檔案往往超過 100 MB,需要流式寫入 以 5 MB 為單位分塊寫入,並在寫入完成後觸發背景任務(轉碼、縮圖)
多檔案表單(如問卷) 同時上傳多張圖片與文字說明 使用 List[UploadFile] + Form,迭代處理每個檔案,保留對應的表單欄位值

總結

UploadFile 是 FastAPI 處理檔案上傳的 關鍵利器,透過 filenamecontent_typefile.read()(或 file.seek())等屬性,我們可以:

  • 快速取得檔案原始資訊(檔名、MIME),便於日誌與驗證。
  • 安全且效能友好地讀寫檔案:小檔案直接讀取,大檔案分塊處理,全部保持非同步。
  • 結合表單欄位,同時接受文字與多檔案,滿足大多數實務需求。

在實作時,務必 限制檔案大小、使用分塊讀取、雙重驗證檔案類型,並配合 aiofiles 等非同步 I/O 库,才能打造既安全又高效的檔案上傳 API。掌握這些概念後,你就能在 FastAPI 專案中自信地處理任何檔案上傳情境,為使用者提供流暢且可靠的服務體驗。祝開發順利!