文件上传

啊哈,文件上传这个老生常谈的问题。文件上传的基本原理实际上非常简单。其基本原理如下

  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 究竟如何处理上传?好吧,如果文件相当小,它会将它们存储在 Web 服务器的内存中,否则会存储在临时位置(由 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 扩展实现了成熟的上传机制,该机制允许控制允许上传哪些文件扩展名。