在HTML中,渲染一個文件上傳字段只須要將<input>標籤的type屬性設爲file,即<input type=」file」>。html
這會在瀏覽器中渲染成一個文件上傳字段,單擊文件選擇按鈕會打開文件選擇窗口,選擇對應的文件後,被選擇的文件名會顯示在文件選擇按鈕旁邊。python
在服務器端,能夠和普通數據同樣獲取上傳文件數據並保存。不過須要考慮安全問題,文件上傳的漏洞也是比較流行的攻擊方式。除了常規的CSRF防範,咱們還須要重點關注這幾個問題:驗證文件類型、驗證文件大小、過濾文件名數據庫
在python表單類中建立文件上傳字段時,咱們使用擴展Flask-WTF提供的FileField類,它集成WTForms提供的上傳字段FileField,添加了對Flask的集成。例如:flask
建立上傳表單:瀏覽器
from flask_wtf.file import FileField, FileRequired, FileAllowed class UploadForm(FlaskForm): photo = FileField('Upload Image', validators=[FileRequired(), FileAllowed(['jpg','jpeg','png','gif'])]) submit = SubmitField()
在表單類UploadForm()中建立了一個FileField類的photo字段,用來上傳圖片。安全
和其餘字段相似,須要對文件上傳字段進行驗證。Flask-WTF在flask_wtf.file模塊下提供了兩個文件相關的驗證器,用法以下:bash
咱們使用FileRequired確保提交的表單字段中包含文件數據。處於安全考慮,必須對上傳的文件類型進行限制。若是用戶能夠上傳HTML文件,並且咱們同時提供了視圖函數獲取上傳後的文件,那麼很容易致使XSS攻擊。使用FileAllowed設置容許的文件類型,傳入一個包含容許文件類型的後綴名列表。服務器
Flask-WTF提供的FileAllowed是在服務器端驗證上傳文件,使用HTML5中的accept屬性也能夠在客戶端實現簡單的類型過濾。這個屬性接收MIME類型字符串或文件格式後綴,多個值之間使用逗號分隔,好比:session
<input type=」file」 id=」profile_pic」 name=」profile_pic」 accept=」.jpg, .jpeg, .png, .gif」>app
當用戶單擊文件選擇按鈕後,打開的文件選擇窗口會默認將accept屬性以外的文件過濾掉(其實沒有過濾掉)。
儘管如此,用戶仍是能夠選擇設定以外的文件,因此仍然須要在服務器端驗證。
驗證文件大小,經過設置Flask內置的配置變量MAX_CONTENT_LENGTH,能夠顯示請求報文的最大長度,單位是字節,好比:
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024
當上傳文件的大小超過這個限制後,flask內置的開服務器會中斷鏈接,在生產環境的服務器上會返回413錯誤響應。
在新建立的upload視圖裏,咱們實例化表單類UploadForm,而後傳入模板:
@app.route('/upload', methods=['GET', 'POST']) def upload(): form = UploadForm() return render_template('upload.html',form = form)
在upload.html中渲染上傳表單
{% from 'macros.html' import form_field %} {% extends 'base.html' %} {% block content %} <form method="post" enctype="multipart/form-data"> {{ form.csrf_token }} {{ form_field(form.photo) }}<br> {{ form.submit }}<br> </form> {% endblock %}
須要注意的是,當表單中包含文件上傳字段時(即type屬性爲file的input標籤)須要將表單的enctype屬性設爲」multipart/form-data」,這會告訴瀏覽器將上傳數據發送到服務器,不然僅會把文件名做爲表單數據提交。
和普通的表單數據不一樣,當包含上傳文件字段的表單提價後,上傳的文件須要在請求對象的files屬性(request.files)中獲取。這個屬性(request.files)是Werkzeug提供的ImmutableMultiDict字典對象,存儲字段name鍵值和文件對象的映射,好比:
ImmutableMultiDict([('photo', <FileStorage: u'xiaxiaoxu.JPG' (image/jpeg)>)])
上傳的文件會被Flask解析爲Werkzeug中的FileStorage對象(werkzeug.datastructures.FileStorage)。當手動處理時,須要使用文件上傳字段的name屬性值做爲鍵獲取對應的文件對象。好比:
request.files.get(‘photo’)
當使用Flask-WTF時,它會自動幫咱們獲取對應的文件對象,這裏咱們仍然使用表單類屬性的data屬性獲取上傳文件。處理上傳表單提交請求的upload視圖函數以下:
import os app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads') @app.route('/upload', methods=['GET', 'POST']) def upload(): form = UploadForm() if form.validate_on_submit(): f = form.photo.data filename =random_filename(f.filename) f.save(os.path.join(app.config['UPLOAD_PATH'], filename)) flash('Upload success.') session['filenames'] = [filename] return redirect(url_for('show_images')) return render_template('upload.html', form = form)
裏面的函數在後面說明
當表單經過驗證後,咱們經過form.photo.data獲取存儲上傳文件的FileStorage對象。接下來,咱們須要處理文件名,一般有三種處理方
1)使用原文件名
若是可以肯定文件的來源安全,能夠直接使用原文件名,經過FileStorage對象的filename屬性獲取:
filename = f.filename
2)使用過濾後的文件名
若是要支持用戶上傳文件,咱們必須對文件名進行處理,由於攻擊者可能會在文件名中加入惡意路徑。好比,若是惡意用戶在文件名中加入表示上級目錄的..(好比../../../home/username/.bashrc或../../etc/passwd),那麼當咱們保存文件時,若是這裏表示上級目錄的../數量正確,就會致使服務器上的系統該文件被覆蓋或篡改,還有可能執行惡意腳本。咱們可使用Werkzeug提供的secure_filename()函數對文件名進行過濾,傳遞文件名做爲參數,它會過濾掉全部危險字符,返回「安全的文件名」,以下所示:
>>> from werkzeug import secure_filename
>>> secure_filename('sam!@$%^&.jpg')
'sam.jpg'
>>> secure_filename('sam圖片.jpg')
'sam.jpg'
>>>
3)統一重命名
secure_filename()函數很是方便,它會過濾掉文件名中的非ASCII字符。但若是文件名徹底由非ASCII字符組成,那麼會獲得一個空文件名:
>>> secure_filename('圖像.jpg')
'jpg'
爲了不出現這種狀況,更好的作法是使用統一的處理方式對全部上傳的文件從新命名。隨機文件名有不少種方式生成,下面是一個是python內置的uuid模塊生成隨機文件名的random_filename()函數:
import uuid def random_filename(filename): ext = os.path.splitext(filename)[1] new_filename = uuid.uuid4().hex + ext return new_filename
其中os.path.splitext()和uuid.uuid4()的用法以下:
>>> import os
>>> os.path.splitext('d://sam/sam.jpg')
('d://sam/sam', '.jpg')
>>> import uuid
>>> uuid.uuid4()
UUID('b35f485e-5a79-4d98-8cac-af62be1f0a36')
>>> uuid.uuid4().hex
'62f65743d16e4b388f9f6eabe3f8e5b4'
這個函數接收原文件名做爲參數,使用內置的uuid模塊中的uuid4()方法生成新的文件名,並使用hex屬性獲取十六進制字符串,最後返回包含後綴的新文件名。
UUID(Universally Unique Identifier,通用惟一識別碼)是用來表示信息的128位數字,好比用做數據庫表的主鍵。使用標準方法生成的UUID出現重複的可能性接近0。在UUID的標準中,UUID分爲5個版本,每一個版本使用不一樣的生產方法而且適用於不一樣的場景。咱們使用的uuid4()方法對應的第4個版本:不接受參數而生成的隨機UUID。
在upload視圖中,咱們調用這個函數獲取隨機文件名,傳入原文件名做爲參數:
filename = random_filename(f.filename)
處理完文件名後,是時候將文件保存到文件系統中了。在form目錄下建立一個uploads文件夾,用於保存上傳後的文件。指向這個文件夾的絕對路徑存儲在自定義配置變量UPLOAD_PATH中:
app.config[‘UPLOAD_PATH’] = os.path.join(app.root_path, ‘uploads’)
這裏的路徑經過app.root_path屬性構造,它存儲了程序實例所在腳本的絕對路徑,至關於:
os.path.abspath(os.path.dirname(__file__))。爲了保存文件,須要提早手動建立這個文件夾。
調試:
print "__file__:",__file__ print "app.root_path:",app.root_path
結果:
__file__: D:/flask/FLASK_PRACTICE/form/app.py
app.root_path: D:\flask\FLASK_PRACTICE\form
對FileStorage對象調用save()方法便可保存,傳入包含目標文件夾絕對路徑和文件名在內的完整保存路徑:
f.save(os.path.join(app.config[‘upload_path’], filename))
文件保存後,咱們但願可以顯示長傳後的圖片,爲了讓上傳後的文件可以經過URL獲取,咱們須要建立一個視圖函數來返回上傳後的文件,以下所示:
@app.route('/uploads/<path:filename>') def get_file(filename): return send_from_directory(app.config['UPLOAD_PATH', filename])
這個視圖的做用與Flask內置的static視圖相似,經過傳入的文件路徑返回對應的靜態文件。在這個uploads視圖中,使用Flask提供的send_from_directory()函數來獲取文件,傳入文件的路徑和文件名做爲參數。
在get_file視圖的URL規則中,filename變量使用了path轉換器以支持傳入包含斜線的路徑字符串。
upload視圖裏保存文件後,使用flash()發送一個提示,將文件名保存到session中,最後重定向到show_images視圖。show_images視圖返回的uploaded.html模板中將從session獲取文件名,渲染出上傳後的圖片。
flash('Upload success.') session['filenames'] = [filename] return redirect(url_for('show_images'))
這裏將filename做爲列表傳入session只是爲了兼容下面的多文件上傳示例,這兩個視圖使用同一個模板,使用session能夠在模板中統一從session獲取文件名列表。
在uploaded.html模板裏,咱們將傳入的文件名做爲URL變量,經過上面的get_file視圖獲取文件URL,做爲<img>標籤的src屬性值,以下所示:
<img src="{{ url_for('get_file', filename=filename) }}">
訪問127.0.0.1:5000/upload,打開文件上傳示例,選擇文件並提交後便可看到上傳後的圖片。另外,在uploads文件夾中能夠看到上傳的文件。
提交後,看到圖片
uploads目錄下保存的文件:
下面列一下涉及的文件:
app.py:
from flask_wtf.file import FileField, FileRequired, FileAllowed from flask import send_from_directory class UploadForm(FlaskForm): photo = FileField('Upload Image', validators=[FileRequired(), FileAllowed(['jpg','jpeg','png','gif'])]) submit = SubmitField() import os app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads') import uuid def random_filename(filename): ext = os.path.splitext(filename)[1] new_filename = uuid.uuid4().hex + ext return new_filename @app.route('/uploaded-images') def show_images(): return render_template('uploaded.html') @app.route('/uploads/<path:filename>') def get_file(filename): return send_from_directory(app.config['UPLOAD_PATH'], filename) @app.route('/upload', methods=['GET', 'POST']) def upload(): form = UploadForm() if form.validate_on_submit(): f = form.photo.data filename =random_filename(f.filename) f.save(os.path.join(app.config['UPLOAD_PATH'], filename)) flash('Upload success.') session['filenames'] = [filename] return redirect(url_for('show_images')) return render_template('upload.html', form = form)
uploaded.html:
{% extends 'base.html' %} {% from 'macros.html' import form_field %} {% block title %}Home{% endblock %} {% block content %} {% if session.filenames %} {% for filename in session.filenames %} <a href="{{ url_for('get_file', filename=filename) }}" target="_blank"> <img src="{{ url_for('get_file', filename=filename) }}"> </a> {% endfor %} {% endif %} {% endblock %}
upload.html:
{% from 'macros.html' import form_field %} {% extends 'base.html' %} {% block content %} <form method="post" enctype="multipart/form-data"> {{ form.csrf_token }} {{ form_field(form.photo) }}<br> {{ form.submit }}<br> </form> {% endblock %}
由於Flask-WTF當前版本中並未添加多多文件上傳到額渲染和驗證支持,所以須要在視圖函數中手動獲取文件並進行驗證。
在客戶端,經過在文件上傳字段(type=file)加入multiple屬性,就能夠開啓多選:
<input type=」file」 id=」file」 multiple>
建立表單類時,能夠直接使用WTForms提供的MultipleFileField字段實現,添加一個DataRequired驗證器來確保包含文件:
from wtforms import MultipleFileField class MultiUploadForm(FlaskForm): photo = MultipleFileField('Upload Image', validators={DataRequired()}) submit = SubmitField()
表單提交時,在服務器端的程序中,對request.files屬性調用getlist()方法並傳入字段的name屬性值會返回包含全部上傳文件對象的列表。在multi_upload視圖中,咱們遍歷這個列表,而後逐一對文件進行處理:
from flask import url_for, request, session, flash, redirect from flask_wtf.csrf import validate_csrf from wtforms import ValidationError @app.route('/multi-upload', methods=['GET', 'POST']) def multi_upload(): form = MultiUploadForm() if request.method == 'POST': filenames = [] #驗證CSRF令牌 try: validate_csrf(form.csrf_token.data) except ValidationError: flash('CSRF token error.') return redirect(url_for('multi_upload')) #檢查文件是否存在 if 'photo' not in request.files: flash('This field is required.') return redirect(url_for('multi_upload')) for f in request.files.getlist('photo'): #檢查文件類型 if f and allowed_file(f.filename): filename = random_filename(f.filename) f.save(os.path.join(app.config['UPLOAD_PATH'], filename )) filenames.append(filename) else: flash('Invalid file type:') return redirect(url_for('multi_upload')) flash('Upload success.') session['filenames'] = filenames return redirect(url_for('show_images')) return render_template('upload.html', form=form)
在請求方法爲POST時,咱們對上傳數據進行手動驗證,包含下面幾步:
1) 手動調用flask_wtf.csrf.validate_csrf驗證CSRF令牌,傳入表單中csrf_token隱藏字段的值。若是拋出wtforms.ValidationError異常則代表驗證未經過。
2) 其中if ‘photo’ not in request.files用來確保字段中包含文件數據(至關於FileRequired驗證器),若是用戶沒有選擇文件就提交表單則request_files將是空(實際上,不選擇文件,點擊提交,會觸發瀏覽器內置提示)。
3) if f用來確保文件對象存在,這裏也能夠檢查f是不是FileStorage實例。
4) allowed_file(f.filename)調用了allowed_file()函數,傳入文件名。這個函數至關於FileAllowed驗證器,用來驗證文件類型,返回布爾值。
allowed_file()函數定義:
app.config['ALLOWED_EXTENSIONS'] = ['png', 'jpg', 'jpeg', 'gif'] def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
在上面的一個驗證語句裏,若是沒有經過驗證,咱們使用flash()函數顯示錯誤消息,而後重定向到multi_uplaod視圖。
filesnames[]列表是爲了方便測試,保存上傳後的文件名到session中。
訪問127.0.0.1:5000/multi-upload,單擊按鈕選擇多個文件,當上傳的文件經過驗證時,程序會重定向到show_images視圖,這個視圖返回的uploaded.html模板中將從session獲取全部文件名,渲染出全部上傳後的圖片。
在新版本的Flask-WTF發佈後,能夠和上傳單個文件相同的方式處理表單。好比可使用Flask-WTF提供的的MultipleFileField來建立多文件上傳的字段,使用相應的驗證器對文件進行驗證。在視圖函數中,能夠繼續使用form.validate_on_submit()來驗證表單,並經過form.photot.data來獲取字段的數據:包含全部上傳文件對象(werkzeug.datastructures.FileStorage)的列表。
多文件上傳處理一般會使用JavaScript庫在客戶端進行預驗證,並添加進度條來優化用戶體驗。