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

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

  1. Тег <form> помечается символом enctype=multipart/form-data, а <input type=file> помещается в эту форму.

  2. Приложение получает доступ к файлу из словаря files на объекте запроса.

  3. использовать метод save() для постоянного сохранения файла в файловой системе.

Мягкое введение

Давайте начнем с очень простого приложения, которое загружает файл в определенную папку upload и отображает файл пользователю. Давайте рассмотрим код загрузки для нашего приложения:

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 the user does not select a file, the browser submits an
        # empty file without a 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('download_file', name=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'

Мы хотим иметь возможность обслуживать загруженные файлы, чтобы они могли быть загружены пользователями. Мы определим представление download_file для обслуживания файлов в папке upload по имени. 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-сервер для обслуживания файлов, вы можете зарегистрировать конечную точку download_file как build_only, чтобы url_for работала без функции представления.

app.add_url_rule(
    "/uploads/<name>", endpoint="download_file", build_only=True
)

Улучшение загрузки

Changelog

Добавлено в версии 0.6.

Как именно 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 0.6, но может быть реализована и в более ранних версиях путем создания подкласса объекта запроса. Для получения дополнительной информации об этом обратитесь к документации Werkzeug по работе с файлами.

Бары прогресса загрузки

Некоторое время назад у многих разработчиков возникла идея читать входящий файл небольшими кусками и хранить прогресс загрузки в базе данных, чтобы иметь возможность опрашивать прогресс с помощью JavaScript с клиента. Клиент спрашивает сервер каждые 5 секунд, сколько он передал, но это то, что он уже должен знать.

Более простое решение

Сейчас есть лучшие решения, которые работают быстрее и надежнее. Существуют библиотеки JavaScript, например jQuery, в которых есть плагины для форм, облегчающие построение прогресс-бара.

Поскольку общий шаблон для загрузки файлов существует почти без изменений во всех приложениях, работающих с загрузкой, существуют также некоторые расширения Flask, которые реализуют полноценный механизм загрузки, позволяющий контролировать, какие расширения файлов разрешены для загрузки.

Вернуться на верх