#################################### 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 に以下の設定はが必要になります。 .. code-block:: python # ストレージを 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 を切り換えるようにします。 .. code-block:: from storages.backends.s3boto import S3BotoStorage StaticRootS3BotoStorage = lambda: S3BotoStorage(location='static') MediaRootS3BotoStorage = lambda: S3BotoStorage(location='media') settings.py で s3.py に設定した値を読み込むようにしましょう。 .. code-block:: DEFAULT_FILE_STORAGE = 'app.s3.MediaRootS3BotoStorage' STATICFILES_STORAGE = 'app.s3.StaticRootS3BotoStorage' この設定をすることで、アップロードした画像は media へ、 元々用意してた静的ファイルは /static/ から呼ばれるようになります。 django-imagekit =============== django-imagekit は画像処理ライブラリですが、よく出来ているので簡単に使えます。 モデルフォームにサムネイルフィールドを追加する ---------------------------------------------- models.py に追加する場合は ImageSpecField を使います。 .. code-block:: python 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= で登録した名前を指定します。 .. code-block:: python 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 に切り換えるだけで対応が可能です。 .. code-block:: # デフォルトが 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 を指定することで処理が保存時のみにすることが可能です。 .. code-block:: IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.Optimistic' キャッシュディレクトリの Prefix を返る -------------------------------------- キャッシュディレクトリはデフォルトでは /CACHE/images/ が指定されます。 何か意図的に変更したい場合に使います。 .. code-block:: IMAGEKIT_CACHEFILE_DIR = ... .. モデルやフォームに追加なしでサムネイルだけを作る方法 .. ---------------------------------------------------- .. .. **S3 を使う場合はオススメ出来ません** .. .. モデルやフォームにフィールドを追加するのではなく template 側でサムネイルを生成する方法があります。 .. .. この方法を使うとサムネイル「だけ」を追加することが出来ます。 .. .. 全てデータはキャッシュに生成されます。 .. .. :: .. .. {% load imagekit %} .. .. {% thumbnail '100x50' entry.img %} .. .. settings.py の IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY は JustInTime に指定して下さい。 .. .. .. code-block:: .. .. IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.JustInTime' .. .. イメージをテンプレート側で生成する .. ---------------------------------- .. .. **S3 を使う場合はオススメ出来ません** .. .. **未検証** .. .. サムネイルだけではなくもう少し細かいところでイメージを生成したいときはこちらが使えます。 .. .. ImageSpec を register した名前を指定することで処理も出来ます。 .. .. :: .. .. {% load imagekit %} .. .. {% generateimage 'core:profile:image_thumbnail' source=source_image %} .. .. settings.py の IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY は JustInTime に指定して下さい。 .. .. .. code-block:: .. .. IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.JustInTime' サンプルプロジェクト ==================== - ジェネリックビューは使ってない - HTML は凄く適当 ソースコード ------------ project/core/views.py .. code-block:: python # 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 .. code-block:: python # 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 .. code-block:: python # coding=utf8 from django import forms from .models import Entry class EntryForm(forms.ModelForm): class Meta: model = Entry project/core/templates/core/home.html .. code-block:: html {% for entry in entries %}

{% endfor %} project/core/templates/core/upload.html .. code-block:: html
{% csrf_token %} {% for field in form %}
{{ field.errors }} {{ field.label_tag }}: {{ field }}
{% endfor %}

project/core/settings.py .. code-block:: python 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 .. code-block:: python # coding=utf8 from storages.backends.s3boto import S3BotoStorage StaticRootS3BotoStorage = lambda: S3BotoStorage(location='static') MediaRootS3BotoStorage = lambda: S3BotoStorage(location='media') project/core/urls.py .. code-block:: python # 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