Хостинг картинок за полчаса

  • Tutorial
Сегодня проекты, особенно на начальном этапе, строятся из готовых блоков. Например, умный хостинг или сервис быстрой отправки сообщений в браузер пользователю. Складывая такие блоки по-разному, можно получить совершенно неожиданный результат. И чем больше разных блоков вам доступно, тем разнообразнее могут получаться проекты. Иногда появляются блоки, которыми можно заменить сразу несколько других блоков. И сами эти блоки — такие же проекты, состоящие из других блоков.

imageНасколько просто сейчас сделать такой сервис, как хостинг изображений? В принципе, его и раньше было несложно сделать. Но прогресс не стоит на месте, и за то же самое время теперь можно учесть больше нюансов. Я уже рассказывал о проекте Uploadcare. Это сервис, позволяющий облегчить работу с файлами: загрузку, хранение, обработку и раздачу конечному пользователю. Его и будем использовать в качестве основного блока.

Пример будет написан на Питоне. Во-первых, потому что Питон я знаю лучше всего, во-вторых библиотека pyuploadcare обновляется в первую очередь. На самом деле, для Uploadcare есть библиотеки под разные языки, и все они в open source. Если в нужном вам модуле отсутствует какая-то функциональность, можно дождаться, когда она появится, или дописать самому.

Начнем с создания нового проекта на Django:

$ pip install django pyuploadcare==0.19
$ django-admin.py startproject upload_test
$ cd upload_test/ && chmod u+x ./manage.py 
$ ./manage.py startapp imageshare
$ ./manage.py syncdb --noinput

В settings.py, помимо привычных параметров подключения к базе данных и INSTALLED_APPS, нужно указать публичный и приватный ключ:

UPLOADCARE = {
    'pub_key': 'demopublickey',
    'secret': 'demoprivatekey',
}

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

Проект будет совсем небольшой: на главной странице будет форма для загрузки. После её отправки идентификатор картинки будет сохраняться в базу. Для этого вполне хватит такой модели:

import string
import random
from pyuploadcare.dj import ImageField
from django.db import models

class Image(models.Model):
    slug = models.SlugField(max_length=10, primary_key=True, blank=True)
    image = ImageField(manual_crop="")

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = ''.join(random.sample(string.ascii_lowercase, 6))
        super(Image, self).save(*args, **kwargs)

Как можно заметить, ImageField тут не джанговский, а из пакета pyuploadcare. Я указал только одну настройку: она позволит пользователю самому выбрать область изображения, которую он хочет загрузить. В методе save() генерируется slug для короткой ссылки.

Теперь прекрасное: вьюшка для главной страницы, сохраняющая картинку, и вьюшка, позволяющая её смотреть:

from django.views.generic import DetailView
from django.views.generic.edit import CreateView
from .models import Image

class UploadView(CreateView):
    model = Image

class ImageView(DetailView):
    model = Image

Класс, по 2 строчки. Просто Django — тоже очень качественный блок для ваших проектов. Чтобы форма отображалась, нужен небольшой шаблон, который будет её выводить. Ничего особенного, но нужно указать публичный ключ для виджета и не забыть поставить в тег документа {{ form.media }}. Многие забывают об этом атрибуте.

{% extends "base.html" %}

{% block head %}
    <script>
    UPLOADCARE_PUBLIC_KEY = 'demopublickey';
    </script>
    {{ form.media }}
{% endblock %}

{% block body %}
    <div class="center">
        <form action="." method="post">
            <p>Please, select an image:</p>
            <p>{{ form.image }}</p>
            <p><input type="submit" value="send"></p>
            {{ form.errors }}
        </form>
    </div>
{% endblock %}

Запускаем.

image

Виджет с выбором файлов появился на странице. Но вот сохранение не работает, Джанга ругается: «No URL to redirect to». Оно и понятно, нужно где-то указать, как получить полную ссылку на картинку. Добавим еще один метод к модели.

    @models.permalink
    def get_absolute_url(self):
        return 'detail', (), {'pk': self.pk}

Осталось написать шаблон полного вывода и можно сказать, что цель достигнута.

{% block body %}
    <img src="{{ image.image }}">
{% endblock %}

Ребята из моего инстаграма передают привет.

image

Внимательный читатель заметит, что на все про все ушло максимум минут 15. Wtf, чем же нам занять еще четверть часа?

Можно улучшить страницу загрузки. В нынешнем виде пользователю приходится делать два лишних клика: для открытия виджета и для отправки формы. Можно их убрать. Для этого нужно воспользоваться javascript api виджета:

<script>
(function() {
    uploadcare.start();

    var widget = uploadcare.Widget('#id_image');
    widget.openDialog();
    widget.onChange(function(file) {
        if (file) {
            var form = document.getElementById('upload-form');
            form.submit();
            form.style.display = 'none';
        }
    });
})();
</script>

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

Что еще? Можно сделать немного информативнее страничку просмотра картинки: показать превьюшку вместо полной картинки и вывести немного информации.

{% block body %}
    <h2>Uploaded Image</h2>

    <a href="{{ image.image.cdn_url }}">
        <img align="left" src="{{ image.image.cdn_url }}-/stretch/off/-/resize/260x/"></a>

    <div class="float-info">
        <p>
            <b>Filename</b>: {{ image.image.info.filename }}<br>
            <b>Uploaded</b>: {{ image.image.info.datetime_uploaded|slice:":10" }}<br>
            <b>Original size</b>: {{ image.image.info.size|filesizeformat }}<br>
        </p>

        <p><a href="{{ image.image.cdn_url }}">Full link</a></p>
    </div>
    <br clear="left">
    <p><a href="{% url 'index' %}">Upload another image</a></p>
{% endblock %}


Превьюшка нужного размера получается с помощью указания опций непосредственно в url картинки. Информация получается через метод info. К сожалению, datetime_uploaded передается в виде строки, поэтому пришлось схитрить — вырезать первые 10 символов. По-хорошему нужно было её парсить. Надеюсь, до десятитысячного года кто-нибудь исправит :)

image

Еще одна мелочь, которую можно исправить — правильно обрабатывать ситуацию, когда картинка была удалена. Правильно обрабатывать — значит отдавать ошибку 404 вместо 500. Лучше всего это делать при получении объекта из базы: запрашивать информацию о файле, и, если в ней есть признак того, что файл удален, удалять хранящуюся у нас ссылку. Кроме того, если файл удален достаточно давно, api может вовсе ничего не вернуть. Нужна обработка и такого случая.

class ImageView(DetailView):
    model = Image

    def get_object(self):
        object = super(ImageView, self).get_object()
        try:
            if object.image.is_removed:
                raise ValueError('File was deleted.')
        except (InvalidRequestError, ValueError):
            object.delete()
            raise Http404

        return object


Теперь, пожалуй, можно остановиться. Осталось задействовать последний блок — бесплатный до определенной нагрузки хостинг heroku — и посмотреть результат: iamshare.herokuapp.com. Исходный код тоже доступен, если кому-то интересно посмотреть на все вместе.
Метки:
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 18
  • 0
    С инстаграммом не захотел коннектиться — iamshare.herokuapp.com/view/jngvqk/
    В остальном очень удобный и функциональный сервис.
    А есть какие-то аналогичные хостинги картинок, которые позволяют коннектиться к соцсетям и облачным хранилищам?

    • +1
      Да, инстаграм с утра отдает плохие ответы даже собственному сайту :(
    • +1
      Uploadcare замечательный сервис которым можно пользоваться. Давно про него знаю уже и активно слежу.
      А на вопрос про аналогичные сервисы — есть подобные загрузчики как Uploadcare, позволяющие коннектиться к сторонним сервисам как пример: www.filepicker.io/ для которого тоже есть различные библиотеки, в том числе и для django(django-filepicker).
    • 0
      Анимированные gif не заливаются?
      Error
      Can’t load image
      • 0
        Вообще, заливаются, но после применения операций ресайза или кропа, анимация пропадет. Более того, анимации не будет даже по ссылке Full link, потому что там применяется операция от ручного кропа (/-/crop/500x374/0,0/ в адресе). Наверное надо сделать так, чтобы когда пользователь ничего не «откусил» в предварительном просмотре, отдавалась оригинальная картинка.

        Что касается «Error Can’t load image», то тут что-то непонятное. Пришел один отчет об ошибке, скорее всего это ваша картинка, попробуем разобраться.
      • +3
        cl.ly/image/3h3E0D390Z2t — очень долго (больше минуты) обрабатывается большая картинка (4275х2404), чтобы показать ее превью. За это время сто раз скачать ее можно (1.8 мб). Имхо, нужно отказаться от порочной практике загрузки серверных мощностей на обработку для превью, и просто выгружать ее (или, что еще лучше, для небольших по разрешению фоток обрабатывать, а для больших — выкачивать).

        А так вот — iamshare.herokuapp.com/view/xtfhob/ Но при клике на нее выходит «400: Bad Request At least one of dimensions should be less than 634».
        • 0
          К слову, простое и изящное решение для лоадера, подсмотренное у eviterra.com — писать, почему долго. Например, что-то про то, что «изображение ресайзится» или типа того.
          • 0
            Рома! Есть хорошая новость и плохая. Хорошая: мы наконец-то пофиксили это в версии виджета 0.8.1.2 и на iamshare я обновил виджет. Плохая: я не учел, что на Хероку sqlite хранит данные в оперативной памяти и при обновлении вообще все загруженные сегодня картинки потерлись. Но я прочесал всю историю браузера, нашел твою оригинальную картинку и загрузил её снова: iamshare.herokuapp.com/view/exldtk/ Как видишь, ссылка Full link работает!
          • НЛО прилетело и опубликовало эту надпись здесь
            • +2
              А теперь честно скажите, сколько времени вы потратили. А то это похоже на традиционное программистское «Да я за пять минут!» * ** *** **** *****

              * Если бы у меня сразу была запущена среда разработки
              ** Если бы я не ошибся в вводе ни единого символа
              *** Если бы всё скачиваемое сразу было на компьютере
              **** Если бы я не занимался отладкой и тестированием
              ***** Если бы я не думал, что писать, а сразу знал каждый символ
              • 0
                * если бы это не было прототипом вида hello world.
                Разница между рабочим проектом массового использования и прототипом это несколько порядков.
              • 0
                У меня волосы дыбом.

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

                Если вам предлагают решение, где сделано так, то это либо глупость, либо сознательная диверсия, чтобы на живом проекте storage backend нельзя было безболезненно сменить за счет высокой связности архитектуры.

                Единственное исключение — хранилища, i/o которых работает с значительной задержкой либо с просто большими по объему i/o транзакциями или их последовательностями (гиг передать). Для них должна быть своя логика и свой пул состояний, отличные от стандартной схемы Django.

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

                Генерация превьюх — тяжелая вычислительная операция (с еще одним i/o циклом, кстати), промежуточными состояниями и отказами и ее тоже нужо «отвязывать» чем выше, тем лучше. Там еще может кэш подоткнуться со временем, очереди. Посмотрите, как умные авторы sorl-thumbnail делают. Если решение не похоже на него — выкидывайте.
                • 0
                  Они должны переключаться по щелчку пальца строчкой конфига или галочкой в меню без изменения кода модели.
                  Я бы хотел посмотреть, как на живом проекте переключаются файловые хранилища по щелчку пальца без переноса самих файлов и без миграции данных.

                  Про генерацию превьюх у вас странные рассуждения. Вы уверены, что все правильно поняли? Генерация происходит на сервере uploadcare по параметрам в url. Взгляните еще раз на строчку:

                  <img align="left" src="{{ image.image.cdn_url }}-/stretch/off/-/resize/260x/">
                  • 0
                    >Я бы хотел посмотреть, как на живом проекте переключаются файловые хранилища по щелчку пальца без переноса самих файлов и без миграции данных.
                    Перенос самих файлов, разумеется, нужен, если до этого они не синхронизировались. Все остальное можно переключать моментально, если хранилище описано в рамках storage API.

                    >Про генерацию превьюх у вас странные рассуждения. Вы уверены, что все правильно поняли? Генерация происходит на сервере uploadcare по параметрам в url. Взгляните еще раз на строчку:

                    И выше уже много сложностей и оговорок по поводу того, что они могут, а что нет. Как эта штука себя будет вести если, например, загрузить ленту из 50 картинок с немного измененным масштабом превьюх? Или на больших файлах: фото, сканах, скришотах?
                • +2
                  потрясающий, короткий и дельный guide.
                  естественно, безопасность и архитектуру можно еще долго дорабатывать, о чём сидетельствуют 100500 комментариев, но, как прототип — конечно же отличная работа.
                  • 0
                    Да, для прототипа все просто замечательно и компактно, главное понимать, где он кончится.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.