Основная идея загрузки файлов во 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()