檔案上傳

啊,是的,檔案上傳這個經典問題。檔案上傳的基本概念其實非常簡單。它基本上是這樣運作的:

  1. 一個 <form> 標籤被標記為 enctype=multipart/form-data,並且在該表單中放置一個 <input type=file>

  2. 應用程式從請求物件上的 files 字典存取檔案。

  3. 使用檔案的 save() 方法將檔案永久儲存到檔案系統上的某個位置。

簡介

讓我們從一個非常基礎的應用程式開始,該應用程式將檔案上傳到特定的上傳資料夾,並向使用者顯示檔案。讓我們看看我們應用程式的引導程式碼:

import os
from flask import Flask, flash, request, redirect, url_for
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = '/path/to/the/uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

首先我們需要導入一些模組。大多數應該都很簡單明瞭,werkzeug.secure_filename() 將在稍後解釋。UPLOAD_FOLDER 是我們將儲存上傳檔案的位置,而 ALLOWED_EXTENSIONS 是允許的檔案副檔名集合。

為什麼我們要限制允許的副檔名?如果伺服器直接將資料發送給客戶端,您可能不希望您的使用者能夠上傳任何東西。這樣您可以確保使用者無法上傳會導致 XSS 問題的 HTML 檔案(請參閱 跨網站指令碼(XSS))。另外,如果伺服器執行 .php 檔案,請務必禁止它們,但誰會在他們的伺服器上安裝 PHP 呢,對吧? :)

接下來是檢查副檔名是否有效,以及上傳檔案並將使用者重新導向到上傳檔案 URL 的函數:

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        # If the user does not select a file, the browser submits an
        # empty file without a filename.
        if file.filename == '':
            flash('No selected file')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('download_file', name=filename))
    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''

那麼 secure_filename() 函數實際上做了什麼?現在的問題是存在一個稱為「永遠不要信任使用者輸入」的原則。這對於上傳檔案的檔名也是如此。所有提交的表單資料都可以被偽造,並且檔名可能很危險。目前只需記住:始終使用該函數來保護檔名,然後再將其直接儲存在檔案系統上。

給專業人士的資訊

那麼您有興趣了解 secure_filename() 函數的作用,以及如果您不使用它會有什麼問題嗎?那麼,想像一下有人會將以下資訊作為 filename 發送到您的應用程式:

filename = "../../../../home/username/.bashrc"

假設 ../ 的數量是正確的,並且您將其與 UPLOAD_FOLDER 連接起來,則使用者可能具有修改伺服器檔案系統上他不應該修改的檔案的能力。這確實需要一些關於應用程式外觀的知識,但相信我,駭客是很有耐心的 :)

現在讓我們看看該函數是如何運作的:

>>> secure_filename('../../../../home/username/.bashrc')
'home_username_.bashrc'

我們希望能夠提供上傳的檔案,以便使用者可以下載它們。我們將定義一個 download_file 視圖,以通過名稱提供上傳資料夾中的檔案。url_for("download_file", name=name) 生成下載 URL。

from flask import send_from_directory

@app.route('/uploads/<name>')
def download_file(name):
    return send_from_directory(app.config["UPLOAD_FOLDER"], name)

如果您使用中介軟體或 HTTP 伺服器來提供檔案,您可以將 download_file 端點註冊為 build_only,以便 url_for 在沒有視圖函數的情況下也能運作。

app.add_url_rule(
    "/uploads/<name>", endpoint="download_file", build_only=True
)

改進上傳

變更日誌

在版本 0.6 中新增。

那麼 Flask 究竟是如何處理上傳的呢?如果檔案相當小,它會將它們儲存在網路伺服器的記憶體中,否則會儲存在臨時位置(由 tempfile.gettempdir() 返回)。但是,您如何指定在超過最大檔案大小後中止上傳?預設情況下,Flask 會很樂意接受具有無限記憶體量的檔案上傳,但您可以通過設定 MAX_CONTENT_LENGTH 配置鍵來限制它:

from flask import Flask, Request

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000

上面的程式碼將最大允許的有效負載限制為 16 兆位元組。如果傳輸較大的檔案,Flask 將引發 RequestEntityTooLarge 異常。

連線重設問題

當使用本地開發伺服器時,您可能會收到連線重設錯誤,而不是 413 回應。當使用生產 WSGI 伺服器執行應用程式時,您將獲得正確的狀態回應。

此功能在 Flask 0.6 中新增,但也可以通過子類化請求物件在較舊版本中實現。有關此方面的更多資訊,請查閱 Werkzeug 關於檔案處理的文件。

上傳進度條

前段時間,許多開發人員想到以小區塊讀取傳入的檔案,並將上傳進度儲存在資料庫中,以便能夠從客戶端使用 JavaScript 輪詢進度。客戶端每 5 秒詢問伺服器已傳輸多少,但這是它應該已經知道的事情。

更簡單的解決方案

現在有更好的解決方案,它們更快、更可靠。有一些 JavaScript 函式庫,例如 jQuery,它們具有表單外掛程式,可以簡化進度條的建構。

由於檔案上傳的常見模式在所有處理上傳的應用程式中幾乎保持不變,因此還有一些 Flask 擴充功能實現了完整功能的上傳機制,允許控制允許上傳哪些檔案副檔名。