Загрузка файлов
Да, добрая старая проблема загрузки файлов. Основная идея загрузки файлов на самом деле довольно проста. Она работает примерно так:
- Тег
<form>помечен какenctype=multipart/form-data, и в эту форму помещается<input type=file>. - Приложение получает доступ к файлу из словаря
filesобъекта запроса. - Используйте метод
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 — это набор разрешённых расширений файлов.
Почему мы ограничиваем разрешённые расширения? Скорее всего, вы не хотите, чтобы пользователи могли загружать всё, что угодно, если сервер напрямую отправляет данные клиенту. Таким образом, вы можете убедиться, что пользователи не могут загружать HTML-файлы, которые могут вызвать проблемы с XSS (см. Межсайтовый скриптинг (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 user does not select file, browser also
# submit an empty part without 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('uploaded_file',
filename=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'
Теперь не хватает одного последнего элемента: предоставления загруженных файлов. В upload_file() мы перенаправляем пользователя на url_for('uploaded_file', filename=filename), то есть на /uploads/filename. Итак, мы записываем функцию uploaded_file() для возвращения файла с этим именем. Начиная с Flask 0.5, мы можем использовать функцию, которая делает это за нас:
from flask import send_from_directory
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'],
filename)
В качестве альтернативы вы можете зарегистрировать uploaded_file как build_only правило и использовать SharedDataMiddleware. Это также работает с более старыми версиями Flask:
from werkzeug.middleware.shared_data import SharedDataMiddleware
app.add_url_rule('/uploads/<filename>', 'uploaded_file',
build_only=True)
app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
'/uploads': app.config['UPLOAD_FOLDER']
})
Если вы сейчас запустите приложение, всё должно работать как ожидается.
Улучшение загрузки
Changelog
Новое в версии 0.6.
Итак, как именно Flask обрабатывает загрузки? Ну, он будет хранить их в памяти веб-сервера, если файлы достаточно небольшие, а иначе — во временном месте (как возвращается tempfile.gettempdir()). Но как указать максимальный размер файла, после которого загрузка прерывается? По умолчанию Flask с удовольствием примет загрузку файлов неограниченного размера в памяти, но вы можете ограничить это, установив параметр конфигурации MAX_CONTENT_LENGTH:
from flask import Flask, Request app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
Приведённый выше код ограничит максимальный разрешённый объём до 16 мегабайт. Если передаётся больший файл, Flask выведет исключение RequestEntityTooLarge.
Проблема с разрывом соединения
При использовании локального сервера разработки вместо ответа 413 вы можете получить ошибку разрыва соединения. Вы получите правильный ответ состояния при запуске приложения с сервером WSGI для производства.
Эта функция была добавлена в Flask 0.6, но её можно реализовать и в более старых версиях, создав подкласс объекта запроса. Для получения дополнительной информации ознакомьтесь с документацией Werkzeug по обработке файлов.
Полосы прогресса загрузки
Некоторое время назад многие разработчики имели идею читать входящий файл небольшими частями и сохранять прогресс загрузки в базе данных, чтобы можно было опросить прогресс с помощью JavaScript на клиенте. Короче говоря: клиент спрашивает сервер каждые 5 секунд, сколько уже передано. Вы понимаете иронию? Клиент спрашивает о чём-то, что он уже должен знать.
Более простое решение
Сейчас есть лучшие решения, которые работают быстрее и надёжнее. Существуют JavaScript-библиотеки, такие как jQuery, которые имеют плагины для форм для упрощения создания полосы прогресса.
Поскольку стандартный шаблон для загрузки файлов практически не меняется во всех приложениях, связанных с загрузками, существует также расширение Flask под названием Flask-Uploads, которое реализует полноценный механизм загрузки с белым и чёрным списком расширений и многим другим.
© 2007–2020 Pallets
Licensed under the BSD 3-clause License.
https://flask.palletsprojects.com/en/1.1.x/patterns/fileuploads/