某天在Stack Overflow上看到一个关于Dropzone.js的问题,就去研究了一下,写了一个回答,顺便写了一个小demo。我发现,如果有一个集成Dropzone.js到Flask,并且简化设置步骤的扩展,肯定要比其他上传方式简单的多——于是就有了Flask-Dropzone。
Flask-Dropzone在模板中提供了一些方法来帮助你创建上传区域,引入相关资源。你只需要添加一些配置就可以实现上传类型的过滤,文件大小限制,上传后跳转等功能。当然,你还要自己编写视图函数来处理和保存文件,并进行服务器端的二次验证。
《Flask Web开发实战》中的第3个示例程序(图片社交程序Albumy)使用了这个扩展。
用法说明
安装
$ pip install flask-dropzone
初始化
和其他扩展类似,你可以通过实例化Dropzone类,并传入程序实例app进行初始化:
from flask_dropzone import Dropzone app = Flask(__name__) dropzone = Dropzone(app)
在使用工厂函数创建程序实例时,你也可以使用init_app()方法:
from flask_dropzone import Dropzone
dropzone = Dropzone()
def create_app():
app = Flask(__name__)
dropzone.init_app(app)
return app
引入Dropzone.js资源
你需要自己手动编写引入Dropzone.js的CSS和JavaScript资源的语句。在开发时,或对于玩具项目,你可以可以使用Flask-Dropzone提供的两个快捷方法:
<head>
...
{{ dropzone.load_css() }}
</head>
<body>
...
{{ dropzone.load_js() }}
</body>
创建并美化上传区域
如果你不需要对上传区域的样式有太多控制,那么你只需要在想要渲染上传区域的地方使用dropzone.create()方法:
{{ dropzone.create(action='处理上传文件的路由URL') }}
记得把action的值更改成你要处理文件上传的的URL。你可以使用style()方法为上传区域添加简单的自定义样式:
<head> ... <!-- 在引入Dropzone.js的CSS文件后调用style()方法 --> {{ dropzone.style('border: 2px dashed #0087F7; margin: 10%') }} </head>
上传区域的截图示例如下所示:
在服务器端处理并保存上传文件
当文件被拖拽到上传区域,或是点击上传区域选择上传文件后,这些文件会以AJAX请求的形式发送到你在dropzone.create()方法中使用action参数传入的URL。我们需要在服务器端创建对应的视图函数来处理这些请求,下面是一个最基本的示例:
import os from flask import Flask, request from flask_dropzone import Dropzone app = Flask(__name__) dropzone = Dropzone(app) @app.route('/uploads', methods=['GET', 'POST']) def upload(): if request.method == 'POST': # 如果请求类型为POST,说明是文件上传请求 f = request.files.get('file') # 获取文件对象 f.save(os.path.join('the/path/to/save', f.filename)) # 保存文件 return 'upload template' # 渲染上传页面
上传完成后重定向
这里需要注意的是,因为Dropzone.js通过AJAX请求提交文件,所以你没法在保存文件后将页面重定向。对于这个问题,你可以使用配置变量DROPZONE_REDIRECT_VIEW设置上传完成后跳转到的目标端点,或是添加一个按钮让用户自己点击进行跳转。
服务器端验证
尽管Dropzone.js可以在前端对用户提交的文件进行验证,但为了安全考虑,我们仍然需要在服务器端进行二次验证。在服务器端验证时,如果验证出错,我们不能像往常那样使用flash()函数“闪现”错误消息,因为AJAX请求接受到响应后并不会重载页面,所以不会显示通过flash()函数发送的消息。正确的做法是返回400错误响应,使用错误消息作为响应的主体。下面是一个简单的进行服务器端验证并返回错误消息得示例:
@app.route('/', methods=['POST', 'GET']) def upload(): if request.method == 'POST': f = request.files.get('file') if f.filename.split('.')[1] != 'png': return 'PNG only!', 400 # return the error message, with a proper 4XX code f.save(os.path.join('the/path/to/save', f.filename)) return render_template('index.html')
在上面的代码中,我们验证图片是不是png格式,如果不是就返回一个错误提示,在服务器端会在图片下面看到我们返回的错误消息:
完整的配置列表
Flask-Dropzone提供了丰富的配置变量,你可以使用它们对Dropzone.js进行各类配置。
Name | Default Value | Info |
---|---|---|
DROPZONE_SERVE_LOCAL |
False |
使用Flask-Dropzone提供的load_css()和load_js()方法时,这个变量设置是否使用内置的本地资源,默认从CND加载 |
DROPZONE_MAX_FILE_SIZE |
3 | 允许的文件最大值,单位为MB |
DROPZONE_INPUT_NAME |
file |
上传字段的name值 |
DROPZONE_ALLOWED_FILE_CUSTOM |
False |
是否使用自定义文件类型允许规则,具体见后面 |
DROPZONE_ALLOWED_FILE_TYPE |
'default' |
允许的文件类型,具体见后面 |
DROPZONE_MAX_FILES |
‘null’ | 一次可以上传的文件数量最大值 |
DROPZONE_DEFAULT_MESSAGE |
“Drop files here to upload” | 上传区域显示的提示文字 |
DROPZONE_INVALID_FILE_TYPE |
“You can’t upload files of this type.” | 文件类型错误的错误消息 |
DROPZONE_FILE_TOO_BIG |
“File is too big {{filesize}}. Max filesize: {{maxFilesize}}MiB.” | 文件太大时显示的错误消息 |
DROPZONE_SERVER_ERROR |
“Server error: {{statusCode}}” | 服务器错误的错误消息 |
DROPZONE_BROWSER_UNSUPPORTED |
“Your browser does not support drag’n’drop file uploads.” | 浏览器不支持的错误消息 |
DROPZONE_MAX_FILE_EXCEED |
“Your can’t upload any more files.” | 超过最大文件数量限制的错误消息 |
DROPZONE_UPLOAD_MULTIPLE |
False |
是否在单个请求中发送多个文件,默认一个请求发送一个文件 |
DROPZONE_PARALLEL_UPLOADS |
2 | 当DROPZONE_UPLOAD_MULTIPLE设为True时,设置单个请求包含的文件数量 |
DROPZONE_REDIRECT_VIEW |
None |
上传完成后重定向的模板端点 |
DROPZONE_ENABLE_CSRF |
False |
是否开启CSRF保护 |
这些配置的用法你可以参考Flask-Drozone的文档或是示例程序了解,这里我们仅简单介绍一下对文件类型进行过滤的设置方法。
设置文件类型过滤
Flask-Dropzone内置了一些文件类型(通过MIME定义),可选的值和对应的文件类型如下所示:
- default:默认值,运行所有类型
- image:图片
- audio:音频
- video:视频
- text:文本
- app:程序
你需要为DROPZONE_ALLOWED_FILE_TYPE设置对应的值,比如下面设置仅允许上传图片:
app.config['DROPZONE_ALLOWED_FILE_TYPE'] = 'image'
如果你想要自己定义允许的文件类型列表,那么你需要将DROPZONE_ALLOWED_FILE_CUSTOM设置True,然后传入一个包含允许的文件后缀名列表组成的字符串给DROPZONE_ALLOWED_FILE_TYPE变量,使用逗号分隔多个后缀名,比如:
app.config['DROPZONE_ALLOWED_FILE_CUSTOM'] = True app.config['DROPZONE_ALLOWED_FILE_TYPE'] = 'image/*, .pdf, .txt'
对于平行上传、CSRF保护等内容的具体实现方法你可以参考文档了解。不过,这个项目目前还没有创建完善的文档,暂时只是写到README里,如果你发现了英文语法或拼写错误,欢迎指正,同时也欢迎为项目贡献代码。
示例程序
Flask-Dropzone的Git仓库中的examples目录下包含4个示例程序,分别演示了基本用法、CSRF保护、平行上传和上传完成后跳转四个功能。
源码可以在这里找到:https://github.com/greyli/flask-dropzone/tree/master/examples
相关链接
如何实现上传整个目录?
李老师,您好:
我们需要做一个in_form的dropzone,但是在html里的,加上蓝图前缀的时候,就会报错:werkzeug.routing.BuildError: Could not build url for endpoint ”. 不加蓝图前缀,就add_course,又会提示是否应该是’cam_pus.add_course’
Hello,你得提供完整的报错和相关代码我才能确认问题,如果方便的话,麻烦把这些详细信息发到 HelloFlask 论坛。
李老师,
我想用dropzone单独做一个上传的应用。
项目里,如果表单有上传需求,就先到固定上传应用服务地址上传。上传成功返回文件路径给应用。这样不用在项目里去集成上传的功能。维护模块化。
该如何实现?
请问一下李老师,{{ dropzone.create(action=’处理上传文件的路由URL’) }} ,如果这个路由中需要携带一个参数该怎么处理呢?
比如我的上传路由是这样的:
@main.route(‘/add-photo/’, methods=[‘GET’, ‘POST’])
,这个id在html中已经可以拿到的。
试试用 url_for 生成 URL:{{ dropzone.create(action=url_for(‘add_photo’, id=your_id)) }}
还是报错:
BuildError: Could not build url for endpoint ‘add_photo’ with values [‘id’]. Did you mean ‘main.add_photo’ instead?
html 中我是这样写的:
{{ dropzone.create(action=url_for(‘add_photo’, id=album.id )) }}
views 中:
@main.route(‘/add-photo/’, methods=[‘GET’, ‘POST’])
添加蓝本名前缀:{{ dropzone.create(action=url_for(‘main.add_photo’, id=your_id)) }}
好了,谢谢李老师~
不客气,下次有问题可以发到 HelloFlask 论坛。