上传文件

啊,是的,经典的文件上传问题。文件上传的基本概念实际上非常简单。它基本上是这样工作的

  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 扩展实现了完整的文件上传机制,该机制允许控制允许上传哪些文件扩展名。