Сообщить об ошибке.

Загрузка файлов на сервер во Flask Python

Основная идея загрузки файлов во Flask на самом деле довольно проста. В основном это работает так:

  1. Тег формы помечается как enctype=multipart/form-data, и в эту форму помещается <input type=file>.
  2. Приложение обращается к файлу из словаря Request.files в объекте запроса.
  3. Используйте метод полученного объекта файла .save(), чтобы навсегда сохранить файл где-нибудь в файловой системе.

Содержание:

Ведение в загрузку файлов на Flask.

Создадим простое приложение, которое загружает файл в определенную папку и отображает его пользователю. Смотрим код инициализации веб-приложения:

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 позволяет веб-приложению гибко и эффективно обрабатывать загрузку файлов, а так же обслуживать загруженные файлы. Flask-Uploads умеет создавать различные наборы загрузок - один для вложений документов, другой для фотографий и т. д. Веб-приложение может быть настроено так, чтобы сохранять эти наборы в разных местах и генерировать для них разные URL-адреса.

Параметры конфигурации расширения Flask-Uploads.

Можно гибко настроить поведение расширения 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)

Конфигурация расширения Flask-Uploads.

Конфигурация набора загрузки хранится в приложении. Таким образом, можно использовать наборы загрузки сразу в нескольких приложениях. Используйте функцию 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>

Демо-версия загрузки фотографий с помощью Flask-Uploads и Flask-WTF.

Шаблон 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()