Skip to main content

Asynchronous file upload for Django

Project description

paper-uploads

Асинхронная загрузка файлов для административного интерфейса Django.

PyPI Build Status

Requirements

Features

  • Каждый файл представлен своей моделью, что позволяет хранить вместе с изображением дополнительные данные. Например, alt и title.
  • Загрузка файлов происходит асинхронно и начинается сразу, при выборе файла в интерфейсе администратора.
  • Поля модели, ссылающиеся на файлы, являются производными от OneToOneField и не используют <input type="file">. Благодаря этому, при ошибках валидации прикрепленные файлы не сбрасываются.
  • Загруженные картинки можно нарезать на множество вариаций. Каждая вариация гибко настраивается. Можно указать размеры, качество сжатия, формат, добавить дополнительные pilkit-процессоры, распознавание лиц и другое. См. variations.
  • Совместим с django-storages.
  • Опциональная интеграция с django-rq для отложенной нарезки картинок на вариации.
  • Внутренний подмодуль paper_uploads.cloudinary предоставляет поля и классы, реализующие хранение файлов в облаке Cloudinary.
  • Возможность создавать коллекции файлов. В частности, галерей изображений с возможностью сортировки элементов.

Table of Contents

Installation

Install paper-uploads:

pip install paper-uploads[full]

Add paper_uploads to INSTALLED_APPS in settings.py:

INSTALLED_APPS = [
    # ...
    'paper_uploads',
    # ...
]

Configure paper-uploads in django's settings.py:

PAPER_UPLOADS = {
    'VARIATION_DEFAULTS': {
        'jpeg': dict(
            quality=80,
            progressive=True,
        ),
        'webp': dict(
            quality=75,
        )
    }
}

# Add JS translations
PAPER_LOCALE_PACKAGES = [
   "django.contrib.admin",
   "paper_admin",
   "paper_uploads",
]

FileField

Поле для загрузки файла.

На загружаемые файлы можно наложить ограничения с помощью валидаторов.

from django.db import models
from django.utils.translation import ugettext_lazy as _
from paper_uploads.models import *
from paper_uploads.validators import *


class Page(models.Model):
    report = FileField(_('file'), blank=True, validators=[
        SizeValidator(10*1024*1024)    # up to 10Mb
    ])

При загрузке файла создается экземпляр модели UploadedFile.

UploadedFile

Модель, представляющая загруженный файл.

Поля модели:

Поле Тип Описание
file FileField Ссылка на файл, хранящийся в Django-хранилище.
display_name CharField Удобочитаемое название файла для вывода на сайте.
Пример: Отчёт за 2019 год.
basename CharField Имя файла без пути, суффикса и расширения.
Пример: my_document.
extension CharField Расширение файла в нижнем регистре, без точки в начале.
Пример: doc.
size PositiveIntegerField Размер файла в байтах.
checksum CharField Контрольная сумма файла. Используется для отслеживания изменений файла.
created_at DateTimeField Дата создания экземпляра модели.
modified_at DateTimeField Дата изменения модели.
uploaded_at DateTimeField Дата загрузки файла.

Свойства модели:

Поле Тип Описание
name str Полное имя файла, передающееся в Django storage.
Пример: files/my_document_19sc2Kj.pdf.

Для упрощения работы с загруженными файлами, некоторые методы и свойства стандартного класса FieldFile проксированы на уровень модели:

  • open
  • close
  • closed
  • read
  • seek
  • tell
  • readable
  • writable
  • seekable
  • url
  • path
  • chunks

Таким образом, вместо page.report.file.url можно использовать page.report.url.

Поддерживается протокол контекстного менеджера:

page = Page.objects.first()
with page.report.open() as fp:
    print(fp.read(10))

ImageField

Поле для загрузки изображений.

Во многом аналогично FileField. Может хранить ссылку как на единственное изображение (подобно стандартному полю ImageField), так и на семейство вариаций одного изображения, созданных из исходного с помощью библиотеки variations.

from django.db import models
from django.utils.translation import ugettext_lazy as _
from paper_uploads.models import *


class Page(models.Model):
    image = ImageField(_('single image'), blank=True)

При загрузке изображения создается экземпляр модели UploadedImage.

UploadedImage

Модель, представляющая загруженное изображение.

Поля модели:

Поле Тип Описание
file FileField Ссылка на файл, хранящийся в Django-хранилище.
title CharField Название изображения, которое можно вставить в атрибут title тэга <img>.
description CharField Описание изображения, которое можно вставить в атрибут alt тэга <img>.
width PositiveSmallIntegerField Ширина загруженного изображения.
height PositiveSmallIntegerField Высота загруженного изображения.
basename CharField Имя файла без пути, суффикса и расширения.
Пример: my_image.
extension CharField Расширение файла в нижнем регистре, без точки в начале.
Пример: jpg.
size PositiveIntegerField Размер файла в байтах.
checksum CharField Контрольная сумма файла. Используется для отслеживания изменений файла.
created_at DateTimeField Дата создания экземпляра модели.
modified_at DateTimeField Дата изменения модели.
uploaded_at DateTimeField Дата загрузки файла.

Свойства модели:

Поле Тип Описание
name str Полное имя файла, передающееся в Django storage.
Пример: images/my_image_19sc2Kj.jpg.

По аналогии с FileField, модель UploadedImage проксирует методы и свойства стандартного класса FieldFile.

Variations

Вариация - это дополнительное изображение, которое получается из оригинального путём определенных трансформаций.

Вариации описываются словарем variations поля ImageField:

from django.db import models
from django.utils.translation import ugettext_lazy as _
from paper_uploads.models import *


class Page(models.Model):
    image = ImageField(_('image with variations'),
        blank=True,
        variations=dict(
            desktop=dict(
                size=(1600, 0),
                clip=False,
                jpeg=dict(
                    quality=80,
                    progressive=True
                ),
            ),
            tablet=dict(
                size=(1024, 0),
                clip=False,
                jpeg=dict(
                    quality=75,
                ),
            ),
            mobile=dict(
                size=(640, 0),
                clip=False,
            )
        )
    )

Со списком допустимых опций для вариаций можно ознакомиться в библитеке variations.

Нарезка изображения на вариации происходит при его загрузке. Добавление новых вариаций (равно как изменение существующих) для поля, в которое уже загружен файл, не даст результата. Заново создать вариации можно несколькими способами.

  1. Вызвать метод recut() из экземпляра UploadedImage:

    page = Page.objects.first()
    page.image.recut()
    

    При вызове этого метода все файлы вариаций для указанного экземпляра создаются заново.

    Если используется библиотека django-rq, то можно вызвать метод recut_async(). Он добавит задачу обновления вариаций в очередь.

  2. Выполнить management-команду recreate_variations:

    python3 manage.py recreate_variations app.page --field=report
    

    Эта команда создаёт вариации для всех экземпляров указанной модели.

К вариациям можно обращаться прямо из экземпляра UploadedImage:

page = Page.objects.first()
print(page.image.desktop.url)

Using Redis Queue for variation update

Если загрузка изображений происходит достаточно часто и количество вариаций для каждого изображения велико, то процесс создания вариаций может занимать значительное время. Это может оказать негативное влияние на производительность веб-сервера и даже послужить зоной для DoS-атаки.

Для того, чтобы стабилизировать процесс загрузки изображений, рекомендуется создавать вариации асинхронно, в отдельном процессе. Эту задачу можно решить с помощью django-rq.

pip install django-rq
# settings.py
PAPER_UPLOADS = {
    # ...
    "RQ_ENABLED": True,
    "RQ_QUEUE_NAME": "default"
}

Variation versions

Допустим, у нас есть изображение, которое нужно отобразить в трех вариантах: desktop, tablet и mobile. Если мы хотим поддерживать дисплеи Retina, нам нужно добавить ещё три вариации для размера 2x. Если мы также хотим использовать формат WebP (сохранив исходный формат для обратной совместимости), то общее количество вариаций достигает 12.

Поскольку Retina-вариации отличаются от обычных только увеличенным на постоянный коэффициент размером, а WebP-вариации — принудительной конвертацией в формат WebP, мы можем создавать эти вариации автоматически.

Для этого нужно объявить перечень версий, которые нужно сгенерировать, в новом параметре вариации versions. Поддерживаются следующие значения: webp, 2x, 3x, 4x.

from django.db import models
from django.utils.translation import ugettext_lazy as _
from paper_uploads.models import *


class Page(models.Model):
    image = ImageField(_('image'), blank=True,
        variations=dict(
            desktop=dict(
                # ...
                versions={'webp', '2x', '3x'}
            )
        )
    )

Приведенный выше код создаст следующие вариации:

  • desktop - оригинальная вариация
  • desktop_webp - WebP-версия оригинальной вариации
  • desktop_2x - Retina 2x
  • desktop_webp_2x - WebP-версия Retina 2x
  • desktop_3x - Retina 3x
  • desktop_webp_3x - WebP-версия Retina 3x

NOTE: Retina-суффикс всегда следует после суффикса webp.

Если необходимо переопределить какие-то параметры дополнительной вариации, то придётся объявлять вариацию явно:

from django.db import models
from django.utils.translation import ugettext_lazy as _
from paper_uploads.models import *


class Page(models.Model):
    image = ImageField(_('image'), blank=True,
        variations=dict(
            desktop=dict(
                size=(800, 600),
                versions={'webp', '2x', '3x'}
            ),
            desktop_2x=dict(
                size=(1600, 1200),
                jpeg=dict(
                    quality=72
                )
            )
        )
    )

Collections

Коллекция — это модель, группирующая экземпляры других моделей (элементов коллекции). В частности, с помощью коллекции можно создать фото-галерею или список файлов.

Для создания коллекции необходимо создать класс, унаследованный от Collection и объявить модели элементов, которые могут входить в коллекцию. Синтаксис объявления элементов подобен добавлению полей:

from paper_uploads.models import *


class PageFiles(Collection):
    svg = CollectionItem(SVGItem)
    image = CollectionItem(ImageItem)
    file = CollectionItem(FileItem)

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

В примере выше, коллекция PageFiles может содержать элементы трех классов: FileItem, ImageItem и SVGItem. У элементов коллекции с типом FileItem в поле item_type будет записано значение file, у элементов ImageItem - image и т.п.

Порядок подключения классов элементов к коллекции имеет значение: первый класс, чей метод file_supported() вернет True, определит модель загружаемого файла. По этой причине FileItem должен указываться последним, т.к. он принимает любые файлы.

Вместе с моделью элемента, в поле CollectionItem можно указать валидаторы и дополнительные параметры (в словаре options), которые могут быть использованы для более детальной настройки элемента коллекции.

Полученную коллекцию можно подключать к моделям с помощью CollectionField:

from django.db import models
from paper_uploads.models import *


class PageFiles(Collection):
    svg = CollectionItem(SVGItem)
    image = CollectionItem(ImageItem)
    file = CollectionItem(FileItem)


class Page(models.Model):
    files = CollectionField(PageFiles)

В состав библиотеки входят следующие классы элементов:

  • FileItem. Может хранить любой файл. Из-за этого при подключении к коллекции этот тип должен быть подключен последним.
  • SVGItem. Функционально иденичен FileItem, но в админке вместо абстрактной иконки показывается само SVG-изображение.
  • ImageItem. Для хранения изображения с возможностью нарезки на вариации.

Вариации для изображений коллекции можно указать двумя способами:

  1. в атрибуте класса коллекции VARIATIONS:

    from paper_uploads.models import *
    
    class PageGallery(Collection):
        VARIATIONS = dict(
            mobile=dict(
                size=(640, 0),
                clip=False
            )
        )
        image = CollectionItem(ImageItem)
    
  2. в дополнительных параметрах поля CollectionItem по ключу variations:

    from paper_uploads.models import *
    
    class PageGallery(Collection):
        image = CollectionItem(ImageItem, options={
            'variations': dict(
                mobile=dict(
                    size=(640, 0),
                    clip=False
                )
            )
        })
    

Вариации, указанные первым способом (через VARIATIONS коллекции), используются всеми классами элементов-изображений по умолчанию. Но, если конкретный элемент коллекции объявляет свои собственные вариации (вторым методом), использоваться будут только они.

ImageCollection

Для коллекций, предназначенных исключительно для изображений, доступна модель для наследования ImageCollection. К ней уже подключен класс элементов-изображений.

from paper_uploads.models import *


class PageGallery(ImageCollection):
    VARIATIONS = dict(
        wide=dict(
            size=(1600, 0),
            clip=False,
        ),
        desktop=dict(
            size=(1280, 0),
            clip=False,
        ),
        tablet=dict(
            size=(960, 0),
            clip=False,
        ),
        mobile=dict(
            size=(640, 0),
        )
    )

Наследование от Collection на самом деле создает proxy-модель. Это позволяет не создавать для каждой коллекции отдельную таблицу в БД, но делает невозможным добавление к модели коллекции дополнительных полей.

Чтобы экземпляры коллекций не смешивались при выполнении SQL-запросов, менеджер objects в классе Collection был переопределен, для того, чтобы принимать во внимание ContentType коллекции и выполнять операции только над теми коллекциями, класс которых соответствует текущему.

# Вернет только экземпляры класса MyCollection
MyCollection.objects.all()

# Вернет абсолютно все экземпляры коллекций, всех подклассов Collection
MyCollection._base_manager.all()

Programmatically upload files

from paper_uploads.models import *
from . models import Page


# file / image
with open('file.doc', 'rb') as fp:
    file = UploadedFile(
        owner_app_label=Page._meta.app_label,
        owner_model_name=Page._meta.model_name,
        owner_fieldname="file",  # имя поля модели Page, в которое будет помещен файл
    )
    file.attach_file(fp)
    file.save()


# gallery
gallery = PageGallery.objects.create()
with open('image.jpg', 'rb') as fp:
    item = ImageItem()
    item.attach_to(gallery)
    item.attach_file(fp)
    item.save()

Management Commands

check_uploads

Запускает комплексную проверку загруженных файлов и выводит результат.

Список производимых тестов:

  • загруженный файл существует в файловой системе
  • для изображений существуют все файлы вариаций
  • модель-владелец (указанная в owner_app_label и owner_model_name) существует
  • в модели-владельце существует поле owner_fieldname
  • существует единственный экземпляр модели-владельца со ссылкой на файл
  • у элементов коллекций указан существующий и допустимый item_type
  • модель элементов коллекций идентична указанной для item_type

При указании ключа --fix-missing все отсутствующие вариации изображений будут автоматически перенарезаны из исходников.

python3 manage.py check_uploads --fix-missing

clean_uploads

Находит мусорные записи в БД (например те, у которых нет владельца) и предлагает их удалить.

Владелец загруженного файла устанавливается в момент сохранения страницы в админке. А это происходит позже фактической загрузки файла на сервер. Как следствие, в течение некоторого времени файл будет являться "сиротой". Для того, чтобы такие файлы не удалялись, команда clean_uploads отсеивает файлы, загруженные за последние 30 минут. Указать свой интервал фильтрации (в минутах) можно через ключ --min-age.

python3 manage.py clean_uploads --min-age=60

recreate_variations

Перенарезает вариации для указанных моделей. Модель указывается в формате app_label.model_name.

Если модель является коллекцией, необходимо указать параметр --item-type:

python3 manage.py recreate_variations 'app.Photos' --item-type='image'

Для обычных моделей необходимо указать параметр --field:

python3 manage.py recreate_variations 'app.Page' --field='image'

По умолчанию перенарезаются все возможные вариации для каждого экземпляра указанной модели. Можно указать конкретные вариации, которые нужно перенарезать:

python3 manage.py recreate_variations 'app.Page' --field='image' --variations big small

Также, изображения можно перенарезать через код, для конкретных экземпляров UploadedImage или ImageItem:

# перенарезка `big` и `medium` вариаций поля ImageField
page.image.recut(['big', 'medium'])

# перенарезка всех вариаций для всех картинок коллекции
for image in page.gallery.get_items('image'):
    image.recut()

remove_variations

Удаление файлов вариаций. Параметры аналогичны параметрам recreate_variations.

python3 manage.py remove_variations 'app.Page' --field='image'

Validators

Для добавления ограничений на загружаемые файлы применяются специальные валидаторы:

  • SizeValidator - задает максимально допустимый размер файла в байтах.
  • ExtensionValidator - задает допустимые расширения файлов.
  • MimeTypeValidator - задает допустимые MIME типы файлов.
  • ImageMinSizeValidator - устанавливает минимальный размер загружаемых изображений.
  • ImageMaxSizeValidator - устанавливает максимальный размер загружаемых изображений.
from django.db import models
from django.utils.translation import ugettext_lazy as _
from paper_uploads.models import *
from paper_uploads.validators import *

class Page(models.Model):
    image = ImageField(_('image'), blank=True, validators=[
        SizeValidator(10 * 1024 * 1024),   # max 10Mb
        ImageMaxSizeValidator(800, 800)    # max dimensions 800x800
    ])


class PageGallery(Collection):
    file = CollectionItem(FileItem, validators=[
        SizeValidator(10 * 1024 * 1024),
    ])

Cloudinary

Во встроенном модуле paper_uploads.cloudinary описаны поля и классы, позволяющие загружать файлы и картинки в облачный сервис Cloudinary. В этом случае нарезка вариаций не имеет смысла и поэтому недоступна.

Installation

  1. pip install cloudinary
  2. Добавить paper_uploads.cloudinary и cloudinary в INSTALLED_APPS.
    INSTALLED_APPS = [
        # ...
        'paper_uploads',
        'paper_uploads.cloudinary',
        'cloudinary',
        # ...
    ]
    
  3. Задать данные учетной записи Cloudinary
    CLOUDINARY = {
       'cloud_name': 'mycloud',
       'api_key': '012345678901234',
       'api_secret': 'g1rtyOCvm4tDIfCPFFuh4u1W0PC',
       'sign_url': True,
       'secure': True
    }
    

Model fields

from django.db import models
from paper_uploads.cloudinary.models import *

class Page(models.Model):
    file = CloudinaryFileField(_('file'), blank=True)
    media = CloudinaryMediaField(_('media'), blank=True)
    image = CloudinaryImageField(_('image'), blank=True)

Дополнительные параметры загрузки Cloudinary можно задать с помощью параметра cloudinary:

from django.db import models
from paper_uploads.cloudinary.models import *


class Page(models.Model):
    file = CloudinaryFileField(_('file'), blank=True, cloudinary={
        "folder": "page/files/%Y-%m-%d"
    })

Внимание! Следует быть осторожным, явно указывая опции type и resource_type. Изменение этих опций может привести к невозможности удалить уже загруженные в это поле файлы.

Чтобы предотвратить генерацию трасформаций конечными пользователями, рекомендуется включить в вашем аккаунте Cloudinary Strict transformations.

Collections

В модуле объявлено три класса элементов коллекции: CloudinaryFileItem, CloudinaryImageItem и CloudinaryMediaItem (для аудио и видео).

NOTE: В отличие от библиотеки PIL, Cloudinary поддерживает загрузку SVG-файлов как изображений. Поэтому для SVG-файлов отдельный класс не нужен.

Классы элементов коллекций Cloudinary используются также как обычные:

from paper_uploads.models import *
from paper_uploads.cloudinary.models import *


class PageFiles(Collection):
    image = CollectionItem(CloudinaryImageItem)
    file = CollectionItem(CloudinaryFileItem)


class Page(models.Model):
    files = CollectionField(PageFiles)

Также, как и для обычных коллекций, для Cloudinary объявлено два класса готовых коллекций: CloudinaryCollection и CloudinaryImageCollection. CloudinaryCollection может хранить любые файлы, а CloudinaryImageCollection — только изображения.

from paper_uploads.models import *
from paper_uploads.cloudinary.models import *


class PageFiles(CloudinaryCollection):
    pass


class PageGallery(CloudinaryImageCollection):
    pass


class Page(models.Model):
    files = CollectionField(PageFiles)
    gallery = CollectionField(PageGallery)

Usage

Для вывода ссылки на файл, загруженный в Cloudinary, библиотека содержит шаблонный тэг paper_cloudinary_url:

{% load paper_cloudinary %}

<img src={% paper_cloudinary_url page.image width=1024 crop=fill %}>

Для jinja2:

<img src={% paper_cloudinary_url page.image, width=1024, crop=fill %}>

Также, для jinja2 доступна одноименная глобальная функция:

<img src={{ paper_cloudinary_url(page.image, width=1024, crop='fill') }}>

Settings

Все настройки указываются в словаре PAPER_UPLOADS.

PAPER_UPLOADS = {
    'STORAGE': 'django.core.files.storage.FileSystemStorage',
    'STORAGE_OPTIONS': {},
    'RQ_ENABLED': True,
    'VARIATION_DEFAULTS': {
        'jpeg': dict(
            quality=80,
            progressive=True,
        ),
        'webp': dict(
            quality=75,
        )
    }
}

STORAGE

Путь к классу хранилища Django.

Значение по умолчанию: django.core.files.storage.FileSystemStorage

STORAGE_OPTIONS

Параметры инициализации хранилища.

Значение по умолчанию: {}

FILES_UPLOAD_TO

Путь к папке, в которую загружаются файлы из FileField. Может содержать параметры для даты и времени (см. upload_to).

Значение по умолчанию: files/%Y-%m-%d

IMAGES_UPLOAD_TO

Путь к папке, в которую загружаются файлы из ImageField.

Значение по умолчанию: images/%Y-%m-%d

COLLECTION_FILES_UPLOAD_TO

Путь к папке, в которую загружаются файлы коллекций.

Значение по умолчанию: collections/files/%Y-%m-%d

COLLECTION_IMAGES_UPLOAD_TO

Путь к папке, в которую загружаются изображения коллекций.

Значение по умолчанию: collections/images/%Y-%m-%d

COLLECTION_ITEM_PREVIEW_WIDTH, COLLECTION_ITEM_PREVIEW_HEIGTH

Размеры превью элементов коллекций в админке.

Значение по умолчанию: 180 x 135

COLLECTION_IMAGE_ITEM_PREVIEW_VARIATIONS

Вариации, добавляемые к каждому классу изображений коллекций для отображения превью в админке. Размеры файлов должны совпадать с COLLECTION_ITEM_PREVIEW_WIDTH и COLLECTION_ITEM_PREVIEW_HEIGTH.

RQ_ENABLED

Включает нарезку картинок на вариации через отложенные задачи. Требует наличие установленного пакета django-rq.

Значение по умолчанию: False

RQ_QUEUE_NAME

Название очереди, в которую помещаются задачи по нарезке картинок.

Значение по умолчанию: default

VARIATION_DEFAULTS

Параметры вариаций по умолчанию.

Параметры, указанные в этом словаре, будут применены к каждой вариации, если только вариация их явно не переопределяет.

Значение по умолчанию: None

CLOUDINARY_TYPE

Тип загрузки файлов. Возможные значения: private, upload. Значение по умолчанию: private

CLOUDINARY_TEMP_DIR

Папка в разделе /tmp/, в которую скачиваются файлы из Cloudinary при чтении их содержимого. Доступ к содержимому большого количества файлов из Cloudinary может привести к скачиванию больших объемов данных и захламлению временной папки.

CLOUDINARY_UPLOADER_OPTIONS

Словарь, задающий глобальные параметры загрузки для Cloudinary.

Значение по умолчанию:

PAPER_UPLOADS = {
    "CLOUDINARY_UPLOADER_OPTIONS": {
        "use_filename": True,
        "unique_filename": True,
        "overwrite": True,
        "invalidate": True
    }
}

Development and Testing

After cloning the Git repository, you should install this in a virtualenv and set up for development:

virtualenv .venv
source .venv/bin/activate
pip install -r ./requirements_dev.txt
pip install django-jinja==2.7.0
pre-commit install

Install npm dependencies and build static files:

npm ci
npx webpack

Create .env file:

CLOUDINARY_URL=cloudinary://XXXXXXXXXXXXXXX:YYYYYYYYYYYYYYYYYYYYYYYYYYY@ZZZZZZ?sign_url=1&secure=1

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

paper-uploads-0.5.2.tar.gz (391.0 kB view hashes)

Uploaded Source

Built Distribution

paper_uploads-0.5.2-py2.py3-none-any.whl (476.1 kB view hashes)

Uploaded Python 2 Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page