Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
S3 と非同期サムネイル作成 コトハジメ

S3 と非同期サムネイル作成 コトハジメ

更新:2013-12-08
バージョン:0.1.8
作者:@voluntas
URL:http://voluntas.github.io/

概要

Django 前提

  • 画像のアップロード先を S3 にして欲しい
  • アップロードと同時にサムネイルを生成して欲しい
  • サムネイル生成は非同期であって欲しい

この 3 つの願いはよくある話なのではないでしょうか。

この辺の処理がまとまってるのが見つけられなかったのでまとめてみました。

ゴール

Django 前提

  • S3 アップロードには django-storages を使う
  • サムネイル生成には django-imagekit を使う
  • 非同期処理には django-celery を使う
  • Celery のキューには Redis を使う

これらの 3 つを組み合わせることで画像を S3 にアップロードし、 非同期にサムネイルを生成するという処理を実現させます。

セットアップ

必須(Pillow は後ほど):

$ pip install django django-celery django-imagekit django-storages boto redis celery

オプション:

$ pip install flower

flower は Celery でキューを Web UI から見れる便利なツール。ただし簡易的なのであれば celery events で見られる。

Pillow

Pillow はイメージ変換ライブラリだが、外部ライブラリに依存するので、要注意。

今回は JPEG のサポートが出来れば良い。

macports:

$ sudo port install libjpeg-turbo
$ pip install pillow

...

--------------------------------------------------------------------
PIL SETUP SUMMARY
--------------------------------------------------------------------
version      Pillow 2.2.1
platform     darwin 2.7.5 (default, Aug  1 2013, 01:01:17)
             [GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))]
--------------------------------------------------------------------
--- TKINTER support available
--- JPEG support available
--- ZLIB (PNG/ZIP) support available
*** TIFF G3/G4 (experimental) support not available
--- FREETYPE2 support available
*** LITTLECMS support not available
*** WEBP support not available
*** WEBPMUX support not available
--------------------------------------------------------------------

...

CentOS 6.4:

$ sudo yum install libjpeg-turbo-devel
$ pip install pillow

...

--------------------------------------------------------------------
PIL SETUP SUMMARY
--------------------------------------------------------------------
version      Pillow 2.2.1
platform     linux2 2.7.5 (default, Nov  5 2013, 00:30:50)
             [GCC 4.4.7 20120313 (Red Hat 4.4.7-3)]
--------------------------------------------------------------------
*** TKINTER support not available
--- JPEG support available
--- ZLIB (PNG/ZIP) support available
*** TIFF G3/G4 (experimental) support not available
*** FREETYPE2 support not available
*** LITTLECMS support not available
*** WEBP support not available
*** WEBPMUX support not available
--------------------------------------------------------------------

...

aptitude:

$ sudo ...

pip freeze

$ pip freeze
Django==1.5.5
Pillow==2.2.1
amqp==1.0.13
anyjson==0.3.3
billiard==2.7.3.34
boto==2.15.0
celery==3.0.24
django-appconf==0.6
django-celery==3.0.23
django-imagekit==3.0.4
django-storages==1.1.8
kombu==2.5.16
pilkit==1.1.5
python-dateutil==2.2
pytz==2013.7
redis==2.8.0
six==1.4.1
wsgiref==0.1.2

django-storages

まずは django-storages を使って ImageField や FileField を使ったデータを S3 に上がるようにします。

settings.py に以下の設定はが必要になります。

# ストレージを boto を使った S3 に指定します
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'

# https を有効にします
AWS_S3_SECURE_URLS = True

# 認証クエリーを無効にします
AWS_QUERYSTRING_AUTH = False

# アクセスキーを指定します
AWS_ACCESS_KEY_ID = ''

# シークレットキーを指定します
AWS_SECRET_ACCESS_KEY = ''

# バケット名を指定します
AWS_STORAGE_BUCKET_NAME = ''

この設定をすることで後は普通にアップロードすることで S3 にファイルが置かれるようになります。

media と static

メディアファイルは /media/ で、static ファイルは /static/ から始まるようにしたい場合は以下のようにします。

s3.py というファイルを作り location を切り換えるようにします。

from storages.backends.s3boto import S3BotoStorage

StaticRootS3BotoStorage = lambda: S3BotoStorage(location='static')
MediaRootS3BotoStorage  = lambda: S3BotoStorage(location='media')

settings.py で s3.py に設定した値を読み込むようにしましょう。

DEFAULT_FILE_STORAGE = 'app.s3.MediaRootS3BotoStorage'
STATICFILES_STORAGE = 'app.s3.StaticRootS3BotoStorage'

この設定をすることで、アップロードした画像は media へ、 元々用意してた静的ファイルは /static/ から呼ばれるようになります。

django-imagekit

django-imagekit は画像処理ライブラリですが、よく出来ているので簡単に使えます。

モデルフォームにサムネイルフィールドを追加する

models.py に追加する場合は ImageSpecField を使います。

from django.db import models

from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill


class Entry(models.Model):
    title = models.CharField(max_length=255)
    img = models.ImageField(upload_to='entry/%Y%m%d')
    img_thumbnail = ImageSpecField(source='img',
                                   processors=[ResizeToFill(100, 50)],
                                   format='JPEG',
                                   options={'quality': 60})
  • source は サムネイル変換 対象フィールドを指定します
  • processors は 実際に変換する処理 を指定します
  • format は 画像の形式 を指定します
  • options はそれ以外の設定を指定します

この書き方は ImageSpec と呼ばれる「変換処理」を直接フィールドに指定する方法ですが、 やはり変換処理自体は色々まとめて色々な場面で呼べるようにしたいと考えると思います。

汎用化する場合は ImageSpec を継承したクラスを作ります。 この場合は ImageSpecField には id= で登録した名前を指定します。

from django.db import models

from imagekit import ImageSpec, register
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill


class Entry(models.Model):
    title = models.CharField(max_length=255)
    img = models.ImageField(upload_to='entry/%Y%m%d')
    img_thumbnail = ImageSpecField(source='img',
                                   id='core:profile:image_thumbnail')


class ImageThumbnail(ImageSpec):
    processors = [ResizeToFill(100, 50)]
    format = 'JPEG'
    options = {'quality': 60}

register.generator('core:profile:image_thumbnail', ImageThumbnail)

これを使う事で画像変換処理を綺麗にまとめておくことが出来る用になります。

非同期処理に切り換える

django-imagekit はデフォルトで Celery に対応しています。

settings.py の IMAGEKIT_DEFAULT_CACHEFILE_BACKEND を imagekit.cachefiles.backends.Async に切り換えるだけで対応が可能です。

# デフォルトが imagekit.cachefiles.backends.Simple なので Async に切り換えます
IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'imagekit.cachefiles.backends.Async'

import djcelery
djcelery.setup_loader()

# ローカルの redis をブローカーに使った例
BROKER_URL = 'redis://localhost:6379/0'

あとは celery worker を起動すれば動きます。

セーブ時にサムネイルを保存する

デフォルトのキャッシュファイル戦略が imagekit.cachefiles.strategies.JustInTime となっているため、画像を表示されたタイミングに「いつも」生成します。もちろん既存のデータがあれば生成はしませんが「既存のデータがあるかどうか」も確認が発生します。

そこで変換処理をファイルのアップロード時限定にする戦略に切り換えることが出来ます。

imagekit.cachefiles.strategies.Optimistic を指定することで処理が保存時のみにすることが可能です。

IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.Optimistic'

キャッシュディレクトリの Prefix を返る

キャッシュディレクトリはデフォルトでは /CACHE/images/ が指定されます。

何か意図的に変更したい場合に使います。

IMAGEKIT_CACHEFILE_DIR = ...

サンプルプロジェクト

  • ジェネリックビューは使ってない
  • HTML は凄く適当

ソースコード

project/core/views.py

# coding=utf8

from django.shortcuts import render, redirect
from django.views.decorators.http import require_GET, require_http_methods

from .models import Entry
from .forms import EntryForm


@require_GET
def home(request):
    entries = Entry.objects.all()
    return render(request, 'home.html', {'entries': entries})


@require_http_methods(["GET", "POST"])
def upload(request):
    if request.method == 'POST':
        form = EntryForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return redirect('/')
    else:
        form = EntryForm()
    return render(request, 'upload.html', {'form': form})

project/core/models.py

# coding=utf8

from django.db import models

from imagekit import ImageSpec, register
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill


class Entry(models.Model):
    title = models.CharField(max_length=255)
    img = models.ImageField(upload_to='entry/%Y%m%d')
    img_thumbnail = ImageSpecField(source='img',
                                   id='core:profile:image_thumbnail')

    class Meta:
        ordering = ('title', )

    def __unicode__(self):
        return self.title


class ImageThumbnail(ImageSpec):
    processors = [ResizeToFill(100, 50)]
    format = 'JPEG'
    options = {'quality': 60}

register.generator('core:profile:image_thumbnail', ImageThumbnail)

project/core/forms.py

# coding=utf8

from django import forms

from .models import Entry

class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry

project/core/templates/core/home.html

{% for entry in entries %}
    <div><img src="{{ entry.img_thumbnail.url }}"><br></div>
{% endfor %}

project/core/templates/core/upload.html

<form action="/upload" method="post" enctype="multipart/form-data">{% csrf_token %}
    {% for field in form %}
        <div >
            {{ field.errors }}
            {{ field.label_tag }}: {{ field }}
        </div>
    {% endfor %}
    <p><input type="submit" value="アップロード" /></p>
</form>

project/core/settings.py

INSTALLED_APPS = (
    ...

    'djcelery',
    'imagekit',
    'storages',

    'core',
)

DEFAULT_FILE_STORAGE = 'core.s3.MediaRootS3BotoStorage'
STATICFILES_STORAGE = 'core.s3.StaticRootS3BotoStorage'

AWS_S3_SECURE_URLS = True
AWS_QUERYSTRING_AUTH = False

AWS_ACCESS_KEY_ID = ''
AWS_SECRET_ACCESS_KEY = ''
AWS_STORAGE_BUCKET_NAME = ''

IMAGEKIT_DEFAULT_FILE_STORAGE = DEFAULT_FILE_STORAGE
IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'imagekit.cachefiles.backends.Async'
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.Optimistic'

import djcelery
djcelery.setup_loader()

BROKER_URL = 'redis://localhost:6379/0'

project/core/s3.py

# coding=utf8

from storages.backends.s3boto import S3BotoStorage

StaticRootS3BotoStorage = lambda: S3BotoStorage(location='static')
MediaRootS3BotoStorage  = lambda: S3BotoStorage(location='media')

project/core/urls.py

# coding=utf8

from django.conf.urls import patterns, url

urlpatterns = patterns('',
    url(r'^$', 'core.views.home', name='home'),
    url(r'^upload$', 'core.views.upload', name='upload'),
)

起動

アプリを起動する:

$ python manage.py runserver

redis サーバを立てる:

$ redis-server

celery ワーカを起動する:

$ python manage.py celery worker -E

flower をインストールしている場合:

$ python manage.py celery flower
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment