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

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. Исходный код тоже доступен, если кому-то интересно посмотреть на все вместе.