檔案上傳——Django UploadedFile 介紹
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 23 篇。要開始介紹進階功能了!
現代 Web 服務中,檔案上傳是一個常見的情境。
無論是使用者上傳照片、夾帶附件,檔案上傳都是不可或缺的功能。
本文介紹如何在 Django Ninja 中實現圖片上傳功能,以使用者「上傳大頭貼」(以下都稱為 avatar,因為大頭貼感覺太可愛🥹)API 為例,帶你一步步了解這個過程。
本文所有的程式碼改動,可參考這個 PR。
不過,在此之前,我們要先了解,本章有哪些主題。
第六章「API 進階功能」簡介
對 API 專案而言,進階功能能幫助我們應對複雜場景與大型專案的挑戰。
雖然這是一個入門指南,但我們仍會涵蓋一些常見的進階功能,這些功能不僅提升 API 的靈活性,還能增強了系統效能與使用者體驗。
本章共有 5 篇,介紹 3 個常見的進階功能:
- 卷 23:檔案上傳——Django UploadedFile 介紹(本文)
- 卷 24:分頁(上)Django Ninja 的內建分頁器
- 卷 25:分頁(下)自定義分頁類別
- 卷 26:資料查詢與過濾(上)FilterSchema 介紹
- 卷 27:資料查詢與過濾(下)FilterSchema 多欄位查詢
這些技術不僅對大型專案至關重要,也讓你能在 API 開發中,有效應對多變的需求。
了解了本章重點後,我們開始講述第一個功能——檔案上傳。
檔案上傳的主角——UploadedFile
在 Django Ninja 中,我們可以使用UploadedFile
來接收上傳的檔案,它是 Django UploadedFile
的重新封裝,兩者基本上大同小異。
UploadedFile 概述
UploadedFile 是 Django 處理檔案上傳的核心物件。當使用者上傳檔案時,Django 會自動將檔案封裝成UploadedFile
實例,方便我們進行後續的檔案處理和儲存。
UploadedFile
物件有很多屬性,其中比較常用的有:
name
:上傳檔案的名稱。這可以用來存取檔案的原始檔名。size
:檔案的大小(以位元組計算)。我們可以用來進行檔案大小的驗證。content_type
:檔案的 MIME 類型。這對於驗證上傳檔案的格式非常有用,比如確保檔案是圖片格式。我們後面會用到!read()
:用來讀取檔案的內容。當需要進行自訂檔案處理時,我們可以使用這個方法來取得檔案的二進位資料。chunks()
:當檔案非常大時,使用這個方法可以分塊讀取檔案,避免過多的記憶體佔用。
這些特性使得UploadedFile
非常靈活,能夠應對各種上傳需求,從簡單的圖片上傳到大型檔案的處理。
OK,關於檔案上傳,了解UploadedFile
這個核心元件就足夠了。
開始實作「上傳 avatar」API 的程式碼之前,我們要先進行一些「前置作業」。
檔案上傳,有很多部分其實和 Django 比較有關,而非 Django Ninja 範疇,所以我會重點帶過。
Django 專案相關設定
在實作檔案上傳功能之前,我們需要先告訴 Django:如何處理上傳的檔案。這涉及到MEDIA_URL
和MEDIA_ROOT
的設定。
MEDIA_URL
和MEDIA_ROOT
設定
MEDIA_URL
:這是檔案的 URL 前綴,所有上傳的檔案會透過這個 URL 來存取。MEDIA_ROOT
:這是 Django 伺服器內部,實際儲存上傳檔案的路徑。
在專案的settings.py
新增程式碼如下:
1 | # NinjaForum/settings.py |
如此一來,上傳的檔案會儲存在專案根目錄的media
資料夾中,並通過/media/
路徑來存取。
開發環境下的檔案存取
我們需要 Django 提供的static
方法,來讓開發環境能夠直接存取這些檔案。
在專案的urls.py
加上這行:
1 | # NinjaForum/urls.py |
這段程式碼允許 Django 在開發環境下提供靜態檔案的存取。
建立ImageField
欄位
ImageField
是 Django 專門用來存放圖片的欄位,它實際上是存儲圖片的檔案路徑。類似的欄位還有FileField
。
程式碼如下:
1 | # user/models.py |
搭配之前的settings.py
設定,avatar
欄位會將上傳的圖片存在media/avatars/
資料夾中——這個路徑由MEDIA_ROOT
與欄位upload_to
共同決定。
移至本分支後,專案中已經有新的遷移檔,記得要資料庫遷移:
1 | python manage.py migrate |
附帶一提,ImageField
依賴第三方套件——Pillow。這個套件為欄位提供了處理圖片功能,要先安裝欄位才能正常運作:
1 | pip install Pillow |
使用 Poetry 的讀者,直接在合併後的分支poetry install
即可。
實作:上傳 avatar
前置作業結束,我們終於可以進入重頭戲。
以下是完整的「上傳 avatar」功能:
1 | from ninja import File, Router, UploadedFile |
以下是對這段程式碼的重點解析。
一、UploadedFile
參數定義
在 view 函式的簽名中,UploadedFile
作為 type hint 使用,而avatar_file
參數則代表上傳的檔案。
你可以任意命名avatar_file
這個參數,比如文件範例中是叫file
。我特地取不同名字,就是要強調它的名字是完全可自訂的。
但是!無論你取什麼名稱,在發請求時,body 中的 key 也要使用相同的名稱。
此時的 HTTP 請求應該是長這樣的:(留意 body 中的avatar_file
)
1 | POST /users/1/avatar/ |
Header 中的Content-Type
為multipart/form-data
,而且每個 key-value 對都是以Content-Disposition: form-data;
開頭。這種格式允許在一個請求中同時傳送多個不同類型的資料,包括文字和二進位檔案。
其中的細節,可以參考這篇〈multipart/form-data 初探〉。
二、File
函式
定義=File()
的目的是在告訴 Django Ninja,這個參數應該從 HTTP 請求中的「上傳檔案」部分獲取。類似做法還有我們第 11 篇提到的Query
。
如果沒有這個標記,框架可能無法正確識別並處理上傳的內容。
三、檢查檔案類型
我們使用了UploadedFile
的content_type
屬性,獲得這個 body 內容的檔案類型,確認它是圖片,然後才發行。
1 | # 檢查檔案類型 |
這個方法雖然粗糙,但對於簡單的圖片上傳功能來說已經足夠。
測試上傳純文字文件的結果:
1 | // 400 Bad Request |
在生產環境中,你需要更嚴格的檢查,例如使用專門的圖片處理套件來驗證檔案內容。
四、儲存圖片
最後,我們將圖片賦值給User
的avatar
欄位,並呼叫save()
方法。
Django 會自動處理檔案的儲存,如果名稱重複,它還會自動產生唯一的檔名,然後把檔案放到我們之前指定的位置。
透過 API 上傳了兩次一模一樣的 avatar 後,我們在專案根目錄使用tree
指令看一下結果:
1 | ❯ tree media |
可以看到,第二次上傳的圖片被「自動更名」了。這保證了檔名的唯一性。
在生產環境中,最好自行定義檔案的統一命名格式,以確保更好的管理和安全性。
小結與下一步
本文介紹了如何在 Django Ninja 中實作檔案上傳功能,從前置設定到 API 實作,詳述了UploadedFile
的使用方式。
接下來,我們將介紹另一項進階功能——分頁(Pagination),它能幫助你在回應大量資料時,有效提升效能與使用者體驗。