Pull to refresh

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

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

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

Articles