Spec-Zone.ru › Flask 1.0

Загрузка файлов

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

  1. Тег <form> помечен как enctype=multipart/form-data, и <input type=file> помещается в эту форму.
  2. Приложение получает доступ к файлу из словаря files объекта запроса.
  3. используйте метод 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 = set(['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.0.x/patterns/fileuploads/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API