Основная идея загрузки файлов во Flask на самом деле довольно проста. В основном это работает так:
enctype=multipart/form-data
, и в эту форму помещается <input type=file>
.Request.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
Зачем ограничивать разрешенные к загрузке расширения файлов? Если сервер напрямую будет отправлять загруженные данные клиенту, то могут быть проблемы с XSS. Например загруженные HTML файлы могут содержать вредоносный JavaScript или если на сервере установлен 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':
# проверим, передается ли в запросе файл
if 'file' not in request.files:
# После перенаправления на страницу загрузки
# покажем сообщение пользователю
flash('Не могу прочитать файл')
return redirect(request.url)
file = request.files['file']
# Если файл не выбран, то браузер может
# отправить пустой файл без имени.
if file.filename == '':
flash('Нет выбранного файла')
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))
# если все прошло успешно, то перенаправляем
# на функцию-представление `download_file`
# для скачивания файла
return redirect(url_for('download_file', name=filename))
return '''
<!doctype html>
<title>Загрузить новый файл</title>
<h1>Загрузить новый файл</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
</html>
'''
Так что же на самом деле делает функция secure_filename()
? Проблема в том, что существует принцип, называемый "никогда не доверяйте вводу пользователя". Это также верно для имени загруженного файла. Все отправленные данные формы могут быть подделаны, а имена файлов могут быть опасными. Просто запомните: всегда используйте эту функцию для защиты имени файла перед сохранением его непосредственно в файловой системе.
Дополнительная информация:
Какие могут быть проблемы, если не использовать secure_filename()
? Представьте, что кто-то отправит следующую информацию в качестве имени файла в ваше приложение:
filename = "../../../../home/username/.bashrc"
Предполагая, что вложенность ../
правильная, и Flask присоединит к нему папку UPLOAD_FOLDER
, то пользователь сайта может иметь возможность изменить файл в файловой системе сервера, который он или она не должны изменять. Это действительно требует некоторых знаний о том, как выглядит приложение, но поверьте, хакеры терпеливы...
Посмотрим, как работает функция secure_filename()
:
>>> from werkzeug.utils import secure_filename
>>> 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-сервер для обслуживания файлов, например Nginx, то можно зарегистрировать конечную точку download_file
как build_only
, и тогда url_for()
будет работать без функции-представления.
app.add_url_rule(
"/uploads/<name>", endpoint="download_file", build_only=True
)
Как Flask обрабатывает загрузку? Если файлы достаточно малы, то 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-Uploads позволяет веб-приложению гибко и эффективно обрабатывать загрузку файлов, а так же обслуживать загруженные файлы. Flask-Uploads умеет создавать различные наборы загрузок - один для вложений документов, другой для фотографий и т. д. Веб-приложение может быть настроено так, чтобы сохранять эти наборы в разных местах и генерировать для них разные URL-адреса.
Можно гибко настроить поведение расширения Flask-Uploads прямо из конфигурации создаваемого веб-приложения.
Приведенные ниже настройки применяются для одного набора загрузок, замените FILES
на имя набора (например, UPLOADED_PHOTOS_DEST
):
UPLOADED_FILES_DEST
: параметр указывает на каталог, в котором будут сохранены загруженные файлы.UPLOADED_FILES_URL
: если есть сервер, настроенный для обслуживания файлов в этом наборе, то это URL-адрес, с которого загруженные файла набора будут общедоступны. В конце добавьте косую черту /
.UPLOADED_FILES_ALLOW
: параметр, разрешающий указанные расширения файлов.UPLOADED_FILES_DENY
: параметр, запрещающий расширения файлов.Чтобы сэкономить время на настройку, можно указать две настройки, которые будут применяться как настройки по умолчанию.
UPLOADS_DEFAULT_DEST
: параметр указывает место назначения набора загрузки, если оно не объявлено иным образом. Например, если установить значение /var/uploads
, то набор с именем photos
будет хранить свои загрузки в /var/uploads/photos
.UPLOADS_DEFAULT_URL
: это базовый URL-адрес настроенного сервера, для обслуживания файлов из UPLOADS_DEFAULT_DEST
. Продолжая приведенный выше пример, если директория /var/uploads
доступна по адресу http://example.ru/uploads, то URL-адреса для набор с именем photos
будут начинаться с http://example.ru/uploads/photos. Включите завершающую косую черту.Так же можно установить MAX_CONTENT_LENGTH
, чтобы ограничить размер загружаемых файлов.
Если нет настроенного сервера для обслуживания файлов, то и не нужно устанавливать какие-либо параметры *_URL
. В этом случае, загруженные файлы будут обслуживаться фреймворком Flask. НО если у вас большой трафик загрузки, то для обслуживания файлов лучше использовать более быстрый и производительный сервер, такой как Nginx или Lighttpd.
UploadSet
."Набор загрузок" - это единый набор файлов какой-категории. Его необходимо объявить в коде:
photos = UploadSet('photos', IMAGES)
После этого можно использовать метод .store()
, для сохранения загруженного файла в определенную директорию, после чего извлечь путь до файла и URL-адрес для доступа к нему. Например:
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if request.method == 'POST' and 'photo' in request.files:
filename = photos.save(request.files['photo'])
rec = Photo(filename=filename, user=g.user.id)
rec.store()
flash("Фотография сохранена.")
return redirect(url_for('show', id=rec.id))
return render_template('upload.html')
@app.route('/photo/<id>')
def show(id):
photo = Photo.load(id)
if photo is None:
abort(404)
url = photos.url(photo.filename)
return render_template('show.html', url=url, photo=photo)
Если в конфигурации указано "расположение загрузок по умолчанию" UPLOADS_DEFAULT_DEST
и например, ваше приложение имеет каталог экземпляра приложения, при этом загрузки должны сохраняться в папке upload
каталога экземпляра приложения, то можно быстро перенастроить папку для загрузки, передав аргумент default_dest
конструктору UploadSet
. Например:
media = UploadSet('media', default_dest=lambda app: app.instance_path)
Конфигурация набора загрузки хранится в приложении. Таким образом, можно использовать наборы загрузки сразу в нескольких приложениях. Используйте функцию configure_uploads()
, чтобы загрузить конфигурацию для разных наборов загрузок. Функция configure_uploads()
передает приложению и все наборы загрузок. Вызов configure_uploads
более одного раза безопасен.
from flask_uploads import UploadSet, configure_uploads, IMAGES
photos = UploadSet('photos', IMAGES)
configure_uploads(app, photos)
Если приложение имеет фабрику приложений, то это, именно то место где нужно настраивать расширение Flask-Uploads.
По умолчанию Flask не накладывает никаких ограничений на размер загружаемых данных. Чтобы защитить приложение, можно использовать patch_request_class()
. Если вызывать patch_request_class()
со вторым параметром None
, то для ограничения максимального размера загружаемого файла будет использоваться параметр конфигурации MAX_CONTENT_LENGTH
.
from flask_uploads import patch_request_class
patch_request_class(app, None)
Класс patch_request_class()
также второй параметр может быть числом, которое установить абсолютный предел, но он существует только по причинам обратной совместимости и не рекомендуется для использования. Кроме того, это не обязательно для Flask 0.6 или выше.
Чтобы действительно загрузить файлы, необходимо правильно настроить форму. Форма, которая загружает файлы, должна иметь свой метод, установленный в POST
, и свой тип enctype
, установленный в multipart/form-data
. Если метод формы настроен на GET
, то загрузка вообще не будет работать, а если не установить enctype
, то будет передано только имя файла.
Само поле должно быть <input type=file>
.
<form method=POST enctype=multipart/form-data action="{{ url_for('upload') }}">
...
<input type=file name=photo>
...
</form>
Шаблон index.html
необходимо поместить в папку с именем templates
.
{# index.html #}
{# Создайте папку с именем templates, поместите в нее этот файл #}
<!DOCTYPE html>
<title>Upload File</title>
<h1>Photo Upload</h1>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ form.photo }}
{% for error in form.photo.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{{ form.submit }}
</form>
{% if file_url %}
<br>
<img src="{{ file_url }}">
{% endif %}
Это файл самого приложения для загрузки фотографий с помощью расширений Flask-Uploads и Flask-WTF
# app-upload.py
import os
from flask import Flask, render_template
from flask_uploads import UploadSet, configure_uploads, IMAGES, patch_request_class
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import SubmitField
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'I have a dream'
# нужно будет создать папку с именем 'uploads'
app.config['UPLOADED_PHOTOS_DEST'] = os.path.join(basedir, 'uploads')
photos = UploadSet('photos', IMAGES)
configure_uploads(app, photos)
# максимальный размер файла, по умолчанию 16MB
patch_request_class(app)
class UploadForm(FlaskForm):
photo = FileField(validators=[FileAllowed(photos, 'Image only!'),
FileRequired('File was empty!')])
submit = SubmitField('Upload')
@app.route('/', methods=['GET', 'POST'])
def upload_file():
form = UploadForm()
if form.validate_on_submit():
filename = photos.save(form.photo.data)
file_url = photos.url(filename)
else:
file_url = None
return render_template('index.html', form=form, file_url=file_url)
if __name__ == '__main__':
app.run()