撰寫驗證資料的方式有三種:在表格的定義中撰寫 clean_<field_name> 函式、繼承對應的欄位類別並提供 validate()、撰寫驗證類別並傳入到對應的欄位中。本篇網誌使用第三種方法來驗證使用者上傳的檔案,檢查檔案副檔名、MIME type、檔案大小。
建立 FileValidator
首先安裝必要的 package:
$ pip install python-magic python-magic-bin
建立一個新檔案 validator.py。引入必要的 module:
import magic
from django.utils.deconstruct import deconstructible
from django.template.defaultfilters import filesizeformat
from django.core.exceptions import ValidationError
- magic 用於判斷檔案的 MIME type,比起只判定檔案的附檔名還要來得保險。
- deconstructible decorator 用於宣告等等要建立的類別。
- filesizeformat 幫助轉換容量大小的數值 (byte) 成如 2KB、5MB 等易讀字串。
- ValidationError 則是當取得的檔案驗證沒有通過時,要觸發的例外。
宣告類別,並依照要檢查的項目提供對應的錯誤訊息:
@deconstructible
class FileValidator:
error_messages = {
"file_extension": "無效副檔名: {file_ext},只能為 {allowed_file_ext}",
"mime_type": "無效 MIME type: {mime_type},只能為 {allowed_mime_type}",
"max_size": "上傳的檔案超過 {max_size}",
"min_size": "上傳的檔案小於 {min_size}",
}
初始化函式,指定要檢查的項目:
def __init__(self, allowed_extension: tuple, allowed_mime_type: tuple, \
max_size = None, min_size = None):
self.allowed_extension = allowed_extension
self.allowed_mime_type = allowed_mime_type
self.max_size = max_size
self.min_size = min_size
- allowed_extension:指定允許的副檔名。
- allowed_mime_type:指定允許的 MIME type。除了可以直接查表看想要驗證的檔案屬於哪種 MIME type,也可以直接用 python interpreter 來檢查,參考文末的附錄。
- max_size 與 min_size:檔案的大小限制,單位為 byte,可以不指定。注意:在這邊是收到檔案後才能檢查大小,代表無法透過這個 validator 來防止過大檔案的惡意攻擊,必須在使用的網頁伺服器上事先設定上傳上限。
將驗證的程式碼寫在 __call__() 中,因為 Django 會把我們傳入欄位中的驗證類別的 instance 作為函式直接呼叫。__call__() 需要一個引數供 Django 傳入要被驗證的資料,這裡命名為 file:
def __call__(self, file):
file_ext = file.name.split('.')[-1].lower()
if file_ext not in self.allowed_extension:
raise ValidationError( \
FileValidator.error_messages["file_extension"] \
.format(file_ext = file_ext, \
allowed_file_ext = ", ".join(self.allowed_extension)))
mime_type = magic.from_buffer(file.read(), mime = True)
file.seek(0)
if mime_type not in self.allowed_mime_type:
raise ValidationError( \
FileValidator.error_messages["mime_type"] \
.format(mime_type = mime_type, \
allowed_mime_type = ", ".join(self.allowed_mime_type)))
if self.max_size and file.size > self.max_size:
raise ValidationError( \
FileValidator.error_messages["max_size"] \
.format(max_size = filesizeformat(self.max_size)))
if self.min_size and file.size < self.min_size:
raise ValidationError( \
FileValidator.error_messages["min_size"] \
.format(min_size = filesizeformat(self.min_size)))
上傳的檔案會被轉換成 UploadedFile 類別,利用這個類別取得檔案資訊。在這裡依序驗證副檔名、MIME type、檔案大小,如果上傳的檔案並非想要的,就觸發 ValidationError 例外,並回報錯誤原因。
- 驗證副檔名:透過 UploadedFile.name 取得上傳的檔案檔名,檔名的長度限制可以額外在表格欄位上設定
- 驗證 MINE type:透過 UploadedFile.read() 取得上傳的檔案內容,並傳入給 magic 判斷 MIME type,記得要呼叫 file.seek(0) 將檔案的讀取位置設回開頭。如果檔案較大 (Django 預設為 2.5 MB),應該使用 UploadedFile.chunks() 批次讀取檔案內容,以免一次讀取過大的檔案,佔滿記憶體。再者,magic 可以從部分內容來推定檔案的 MIME type,所以不必完整讀取檔案。
- 驗證檔案大小:透過 UploadedFile.size 取得檔案大小,單位為 byte。
使用 FileValidator
建立表格
產生 FileValidator instance,這個 instance 會驗證上傳的檔案是否為 python code:副檔名必須為 "py",MIME type 必須為 "text/x-python" 或 "text/plain",檔案大小 1MB 以內。將這個 instance 傳入到要建立的表格欄位中的 validators 參數,必須以 list 傳入:# app_name/forms.py
from django import forms
from .validator import FileValidator
class CodeUploadForm(forms.Form):
source_code_validator = FileValidator(("py",), \
("text/x-python", "text/plain"), \
max_size = 1 * 1024 * 1024)
source_code_file = forms.FileField(required = True, max_length = 25, \
validators = [source_code_validator], \
label = _("Python 程式碼"), \
help_text = _("副檔名為 .py, 檔案大小上限為 1 MB, 檔名不能超過 25 字元"))
注意如果只有指定一種副檔名,必須要加逗點才會被視為 tuple。在 view 中使用表格
上傳的檔案都會被放在 request.FILES 中,是 dictionary,key 為表格的欄位變數名稱,value 為 UploadedFile。而「濾過」的檔案則被放在 form.cleaned_data[<field_name>] 中,value 一樣為 UploadedFile。# file app_name/views.py
from django.shortcuts import render
from .forms import CodeUploadForm
def code_upload(request):
if request.method == "POST":
form = CodeUploadForm(request.POST, request.FILES)
if form.is_valid(): # 這裡會驗證表格內容是否有效
# 將檔案存到硬碟中
with("path/to/save/file.py", "wb") as f:
for chunk in form.cleaned_data["source_code_file"].chunks():
f.write(chunk)
return render(reqeust, "code_upload_successful.html")
else:
form = CodeUploadForm()
context = {
"form": form,
}
return render(request, "code_upload.html", context = context)
在 template 中使用表格
這邊要注意的是,form 的 attribute 必須設定 enctype="multipart/form-data",否則 request.FILES 永遠是空的:# file app_name/template/code_upload.html
...
<form action="" enctype="multipart/form-data" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="{% trans '上傳' %}" />
</form>
...
網頁上的結果會像這樣:如果上傳無效的檔案會像這樣:
附錄:使用 magic 取得檔案的 MIME type
想要確認檔案的 MIME type,可以直接在 python interpreter 上測試:
>>> import magic
>>> magic.from_file('manage.py')
'Python script, ASCII text executable, with CRLF line terminators'
>>> magic.from_file('manage.py', mime=True)
'text/x-python'
參考資料
- Django: Validate file type of uploaded file - StackOverflow
- Uploaded Files and Upload Handlers - Django Documentation
- Validators - Django Documentation
- ahupp/python-magic - Github
- Server-side file extension validation in Django 2.1 - Medium
沒有留言:
張貼留言