From 197dfb3485112b25ace247d03955304957e88129 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Sep 2012 21:32:57 -0400 Subject: [PATCH 1/8] Add VALIDATE_ON_ACCESS setting --- imagekit/conf.py | 1 + imagekit/models/fields/__init__.py | 7 ++++++- imagekit/models/fields/files.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 4f3b95c..3d8a651 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,4 +3,5 @@ from appconf import AppConf class ImageKitConf(AppConf): DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.PessimisticImageCacheBackend' + VALIDATE_ON_ACCESS = True CACHE_DIR = 'CACHE/images' diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index faa5fb5..078f814 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,5 +1,6 @@ import os +from django.conf import settings from django.db import models from django.db.models.signals import post_init, post_save, post_delete @@ -18,7 +19,7 @@ class ImageSpecField(object): """ def __init__(self, processors=None, format=None, options=None, image_field=None, pre_cache=None, storage=None, autoconvert=True, - image_cache_backend=None): + image_cache_backend=None, validate_on_access=None): """ :param processors: A list of processors to run on the original image. :param format: The format of the output file. If not provided, @@ -38,6 +39,8 @@ class ImageSpecField(object): :param image_cache_backend: An object responsible for managing the state of cached files. Defaults to an instance of IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND + :param validate_on_access: Should the image cache be validated when it's + accessed? """ @@ -58,6 +61,8 @@ class ImageSpecField(object): self.storage = storage self.image_cache_backend = image_cache_backend or \ get_default_image_cache_backend() + self.validate_on_access = settings.IMAGEKIT_VALIDATE_ON_ACCESS if \ + validate_on_access is None else validate_on_access def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index c213ac3..b46d813 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -31,7 +31,7 @@ class ImageSpecFieldFile(ImageFieldFile): def _require_file(self): if not self.source_file: raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname) - else: + elif self.field.validate_on_access: self.validate() def clear(self): From 0fc29ee7cf220c99225b1256b718c9dc32e4862c Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Sep 2012 21:50:11 -0400 Subject: [PATCH 2/8] Extract useful backend utils --- imagekit/imagecache/__init__.py | 34 ++++++-------------------------- imagekit/utils.py | 35 ++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/imagekit/imagecache/__init__.py b/imagekit/imagecache/__init__.py index cf98a9d..93dfe1b 100644 --- a/imagekit/imagecache/__init__.py +++ b/imagekit/imagecache/__init__.py @@ -1,34 +1,12 @@ -from django.core.exceptions import ImproperlyConfigured -from django.utils.importlib import import_module - -from imagekit.imagecache.base import InvalidImageCacheBackendError, PessimisticImageCacheBackend, NonValidatingImageCacheBackend - -_default_image_cache_backend = None +from ..utils import get_singleton +from .base import InvalidImageCacheBackendError, PessimisticImageCacheBackend, NonValidatingImageCacheBackend def get_default_image_cache_backend(): """ - Get the default image cache backend. Uses the same method as - django.core.file.storage.get_storage_class + Get the default validation backend. """ - global _default_image_cache_backend - if not _default_image_cache_backend: - from django.conf import settings - import_path = settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND - try: - dot = import_path.rindex('.') - except ValueError: - raise ImproperlyConfigured("%s isn't an image cache backend module." % \ - import_path) - module, classname = import_path[:dot], import_path[dot + 1:] - try: - mod = import_module(module) - except ImportError, e: - raise ImproperlyConfigured('Error importing image cache backend module %s: "%s"' % (module, e)) - try: - cls = getattr(mod, classname) - _default_image_cache_backend = cls() - except AttributeError: - raise ImproperlyConfigured('Image cache backend module "%s" does not define a "%s" class.' % (module, classname)) - return _default_image_cache_backend + from django.conf import settings + return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, + 'validation backend') diff --git a/imagekit/utils.py b/imagekit/utils.py index 4ffb556..0fd1b5d 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -3,10 +3,12 @@ import mimetypes import sys import types +from django.core.exceptions import ImproperlyConfigured from django.core.files.base import ContentFile from django.db.models.loading import cache -from django.utils.functional import wraps from django.utils.encoding import smart_str, smart_unicode +from django.utils.functional import wraps +from django.utils.importlib import import_module from .lib import Image, ImageFile, StringIO @@ -371,3 +373,34 @@ def prepare_image(img, format): save_kwargs['optimize'] = True return img, save_kwargs + + +def get_class(path, desc): + try: + dot = path.rindex('.') + except ValueError: + raise ImproperlyConfigured("%s isn't a %s module." % (path, desc)) + module, classname = path[:dot], path[dot + 1:] + try: + mod = import_module(module) + except ImportError, e: + raise ImproperlyConfigured('Error importing %s module %s: "%s"' % + (desc, module, e)) + try: + cls = getattr(mod, classname) + return cls + except AttributeError: + raise ImproperlyConfigured('%s module "%s" does not define a "%s"' + ' class.' % (desc[0].upper() + desc[1:], module, classname)) + + +_singletons = {} + + +def get_singleton(class_path, desc): + global _singletons + cls = get_class(class_path, desc) + instance = _singletons.get(cls) + if not instance: + instance = _singletons[cls] = cls() + return instance From 3103ab29bd208545ddd2e9a70f36ec9581489b90 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Sep 2012 21:54:41 -0400 Subject: [PATCH 3/8] Remove "non-validating" backend It's been superseded by the VALIDATE_ON_ACCESS setting --- imagekit/imagecache/__init__.py | 4 ++-- imagekit/imagecache/base.py | 27 --------------------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/imagekit/imagecache/__init__.py b/imagekit/imagecache/__init__.py index 93dfe1b..4680104 100644 --- a/imagekit/imagecache/__init__.py +++ b/imagekit/imagecache/__init__.py @@ -1,5 +1,5 @@ from ..utils import get_singleton -from .base import InvalidImageCacheBackendError, PessimisticImageCacheBackend, NonValidatingImageCacheBackend +from .base import InvalidImageCacheBackendError, PessimisticImageCacheBackend def get_default_image_cache_backend(): @@ -9,4 +9,4 @@ def get_default_image_cache_backend(): """ from django.conf import settings return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, - 'validation backend') + 'image cache backend') diff --git a/imagekit/imagecache/base.py b/imagekit/imagecache/base.py index f06c9b5..c0ec566 100644 --- a/imagekit/imagecache/base.py +++ b/imagekit/imagecache/base.py @@ -31,30 +31,3 @@ class PessimisticImageCacheBackend(object): def clear(self, file): file.delete(save=False) - - -class NonValidatingImageCacheBackend(object): - """ - A backend that is super optimistic about the existence of spec files. It - will hit your file storage much less frequently than the pessimistic - backend, but it is technically possible for a cache file to be missing - after validation. - - """ - - def validate(self, file): - """ - NonValidatingImageCacheBackend has faith, so validate's a no-op. - - """ - pass - - def invalidate(self, file): - """ - Immediately generate a new spec file upon invalidation. - - """ - file.generate(save=True) - - def clear(self, file): - file.delete(save=False) From 2ad3791d9d5925868142cba26868ad2a16a3f810 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 5 Sep 2012 22:19:46 -0400 Subject: [PATCH 4/8] Reorganize image cache backends --- imagekit/conf.py | 2 +- imagekit/imagecache/__init__.py | 12 ------ imagekit/imagecache/backends/__init__.py | 2 + imagekit/imagecache/{ => backends}/base.py | 18 +++++++-- imagekit/imagecache/backends/celery.py | 35 ++++++++++++++++++ imagekit/imagecache/celery.py | 43 ---------------------- imagekit/models/fields/__init__.py | 2 +- 7 files changed, 54 insertions(+), 60 deletions(-) create mode 100644 imagekit/imagecache/backends/__init__.py rename imagekit/imagecache/{ => backends}/base.py (59%) create mode 100644 imagekit/imagecache/backends/celery.py delete mode 100644 imagekit/imagecache/celery.py diff --git a/imagekit/conf.py b/imagekit/conf.py index 3d8a651..5618bcb 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -2,6 +2,6 @@ from appconf import AppConf class ImageKitConf(AppConf): - DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.PessimisticImageCacheBackend' + DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple' VALIDATE_ON_ACCESS = True CACHE_DIR = 'CACHE/images' diff --git a/imagekit/imagecache/__init__.py b/imagekit/imagecache/__init__.py index 4680104..e69de29 100644 --- a/imagekit/imagecache/__init__.py +++ b/imagekit/imagecache/__init__.py @@ -1,12 +0,0 @@ -from ..utils import get_singleton -from .base import InvalidImageCacheBackendError, PessimisticImageCacheBackend - - -def get_default_image_cache_backend(): - """ - Get the default validation backend. - - """ - from django.conf import settings - return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, - 'image cache backend') diff --git a/imagekit/imagecache/backends/__init__.py b/imagekit/imagecache/backends/__init__.py new file mode 100644 index 0000000..a733a93 --- /dev/null +++ b/imagekit/imagecache/backends/__init__.py @@ -0,0 +1,2 @@ +from .base import * +from .celery import * diff --git a/imagekit/imagecache/base.py b/imagekit/imagecache/backends/base.py similarity index 59% rename from imagekit/imagecache/base.py rename to imagekit/imagecache/backends/base.py index c0ec566..91a319f 100644 --- a/imagekit/imagecache/base.py +++ b/imagekit/imagecache/backends/base.py @@ -1,14 +1,26 @@ +from ...utils import get_singleton from django.core.exceptions import ImproperlyConfigured +def get_default_image_cache_backend(): + """ + Get the default image cache backend. + + """ + from django.conf import settings + return get_singleton(settings.IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND, + 'image cache backend') + + class InvalidImageCacheBackendError(ImproperlyConfigured): pass -class PessimisticImageCacheBackend(object): +class Simple(object): """ - A very safe image cache backend. Guarantees that files will always be - available, but at the cost of hitting the storage backend. + The most basic image cache backend. Files are considered valid if they + exist. To invalidate a file, it's deleted; to validate one, it's generated + immediately. """ diff --git a/imagekit/imagecache/backends/celery.py b/imagekit/imagecache/backends/celery.py new file mode 100644 index 0000000..f3c103d --- /dev/null +++ b/imagekit/imagecache/backends/celery.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +from .base import InvalidImageCacheBackendError, Simple as SimpleBackend + + +def generate(model, pk, attr): + try: + instance = model._default_manager.get(pk=pk) + except model.DoesNotExist: + pass # The model was deleted since the task was scheduled. NEVER MIND! + else: + field_file = getattr(instance, attr) + field_file.delete(save=False) + field_file.generate(save=True) + + +class CeleryBackend(SimpleBackend): + """ + An image cache backend that uses celery to generate images. + + """ + def __init__(self): + try: + from celery.task import task + except: + raise InvalidImageCacheBackendError("Celery validation backend requires the 'celery' library") + if not getattr(CeleryBackend, '_task', None): + CeleryBackend._task = task(generate) + + def invalidate(self, file): + self._task.delay(file.instance.__class__, file.instance.pk, file.attname) + + def clear(self, file): + file.delete(save=False) diff --git a/imagekit/imagecache/celery.py b/imagekit/imagecache/celery.py deleted file mode 100644 index 9dee5ca..0000000 --- a/imagekit/imagecache/celery.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -from imagekit.imagecache import PessimisticImageCacheBackend, InvalidImageCacheBackendError - - -def generate(model, pk, attr): - try: - instance = model._default_manager.get(pk=pk) - except model.DoesNotExist: - pass # The model was deleted since the task was scheduled. NEVER MIND! - else: - field_file = getattr(instance, attr) - field_file.delete(save=False) - field_file.generate(save=True) - - -class CeleryImageCacheBackend(PessimisticImageCacheBackend): - """ - A pessimistic cache state backend that uses celery to generate its spec - images. Like PessimisticCacheStateBackend, this one checks to see if the - file exists on validation, so the storage is hit fairly frequently, but an - image is guaranteed to exist. However, while validation guarantees the - existence of *an* image, it does not necessarily guarantee that you will get - the correct image, as the spec may be pending regeneration. In other words, - while there are `generate` tasks in the queue, it is possible to get a - stale spec image. The tradeoff is that calling `invalidate()` won't block - to interact with file storage. - - """ - def __init__(self): - try: - from celery.task import task - except: - raise InvalidImageCacheBackendError("Celery image cache backend requires the 'celery' library") - if not getattr(CeleryImageCacheBackend, '_task', None): - CeleryImageCacheBackend._task = task(generate) - - def invalidate(self, file): - self._task.delay(file.instance.__class__, file.instance.pk, file.attname) - - def clear(self, file): - file.delete(save=False) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 078f814..49278dc 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -4,7 +4,7 @@ from django.conf import settings from django.db import models from django.db.models.signals import post_init, post_save, post_delete -from ...imagecache import get_default_image_cache_backend +from ...imagecache.backends import get_default_image_cache_backend from ...generators import SpecFileGenerator from .files import ImageSpecFieldFile, ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta From 8a2738ca8ad28ed11cae842f1dec48da86a0a985 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Thu, 6 Sep 2012 00:07:40 -0400 Subject: [PATCH 5/8] Add backend for caching image state --- imagekit/conf.py | 2 ++ imagekit/generators.py | 16 +++++---- imagekit/imagecache/backends/base.py | 49 +++++++++++++++++++++++++--- imagekit/models/fields/files.py | 3 ++ 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/imagekit/conf.py b/imagekit/conf.py index 5618bcb..6c09232 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -3,5 +3,7 @@ from appconf import AppConf class ImageKitConf(AppConf): DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple' + CACHE_BACKEND = None VALIDATE_ON_ACCESS = True CACHE_DIR = 'CACHE/images' + CACHE_PREFIX = 'ik-' diff --git a/imagekit/generators.py b/imagekit/generators.py index 344fc67..6c4a974 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -49,18 +49,20 @@ class SpecFileGenerator(object): content = IKContentFile(filename, imgfile.read(), format=format) return img, content + def get_hash(self, source_file): + return md5(''.join([ + pickle.dumps(self.get_processors(source_file)), + self.format, + pickle.dumps(self.options), + str(self.autoconvert), + ])).hexdigest() + def generate_filename(self, source_file): source_filename = source_file.name filename = None if source_filename: - hash = md5(''.join([ - pickle.dumps(self.get_processors(source_file)), - self.format, - pickle.dumps(self.options), - str(self.autoconvert), - ])).hexdigest() + hash = self.get_hash(source_file) extension = suggest_extension(source_filename, self.format) - filename = os.path.normpath(os.path.join( settings.IMAGEKIT_CACHE_DIR, os.path.splitext(source_filename)[0], diff --git a/imagekit/imagecache/backends/base.py b/imagekit/imagecache/backends/base.py index 91a319f..a489f6d 100644 --- a/imagekit/imagecache/backends/base.py +++ b/imagekit/imagecache/backends/base.py @@ -1,4 +1,6 @@ from ...utils import get_singleton +from django.core.cache import get_cache +from django.core.cache.backends.dummy import DummyCache from django.core.exceptions import ImproperlyConfigured @@ -16,7 +18,39 @@ class InvalidImageCacheBackendError(ImproperlyConfigured): pass -class Simple(object): +class CachedValidationBackend(object): + @property + def cache(self): + if not getattr(self, '_cache', None): + from django.conf import settings + alias = settings.IMAGEKIT_CACHE_BACKEND + self._cache = get_cache(alias) if alias else DummyCache(None, {}) + return self._cache + + def get_key(self, file): + from django.conf import settings + return '%s%s-valid' % (settings.IMAGEKIT_CACHE_PREFIX, file.get_hash()) + + def is_invalid(self, file): + key = self.get_key(file) + cached_value = self.cache.get(key) + if cached_value is None: + cached_value = self._is_invalid(file) + self.cache.set(key, cached_value) + return cached_value + + def validate(self, file): + if self.is_invalid(file): + self._validate(file) + self.cache.set(self.get_key(file), True) + + def invalidate(self, file): + if not self.is_invalid(file): + self._invalidate(file) + self.cache.set(self.get_key(file), False) + + +class Simple(CachedValidationBackend): """ The most basic image cache backend. Files are considered valid if they exist. To invalidate a file, it's deleted; to validate one, it's generated @@ -24,21 +58,26 @@ class Simple(object): """ - def is_invalid(self, file): + def _is_invalid(self, file): if not getattr(file, '_file', None): # No file on object. Have to check storage. return not file.storage.exists(file.name) return False - def validate(self, file): + def _validate(self, file): """ Generates a new image by running the processors on the source file. """ - if self.is_invalid(file): - file.generate(save=True) + file.generate(save=True) def invalidate(self, file): + """ + Invalidate the file by deleting it. We override ``invalidate()`` + instead of ``_invalidate()`` because we don't really care to check + whether the file is invalid or not. + + """ file.delete(save=False) def clear(self, file): diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index b46d813..687295f 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -6,6 +6,9 @@ class ImageSpecFieldFile(ImageFieldFile): super(ImageSpecFieldFile, self).__init__(instance, field, None) self.attname = attname + def get_hash(self): + return self.field.generator.get_hash(self.source_file) + @property def source_file(self): field_name = getattr(self.field, 'image_field', None) From f43bd4ec28468c8e85859a1a4d00cf9d77c3486f Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Sat, 15 Sep 2012 15:09:58 -0400 Subject: [PATCH 6/8] Include source filename in hash --- imagekit/generators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imagekit/generators.py b/imagekit/generators.py index 6c4a974..515bcb7 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -51,6 +51,7 @@ class SpecFileGenerator(object): def get_hash(self, source_file): return md5(''.join([ + source_file.name, pickle.dumps(self.get_processors(source_file)), self.format, pickle.dumps(self.options), From ba9bf1f8771a4d5c89ce53b280a537ea67303059 Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 3 Oct 2012 22:23:11 -0400 Subject: [PATCH 7/8] Add image cache strategies This new feature gives the user more control over *when* their images are validated. Image cache backends are now exclusively for controlling the *how*. This means you won't have to write a lot of code when you just want to change one or the other. --- imagekit/conf.py | 3 +- imagekit/generators.py | 2 +- imagekit/imagecache/actions.py | 23 ++++++++ .../{backends/base.py => backends.py} | 2 +- imagekit/imagecache/backends/__init__.py | 2 - imagekit/imagecache/backends/celery.py | 35 ------------ imagekit/imagecache/strategies.py | 57 +++++++++++++++++++ imagekit/models/fields/__init__.py | 24 +++++--- imagekit/models/fields/files.py | 3 +- 9 files changed, 100 insertions(+), 51 deletions(-) create mode 100644 imagekit/imagecache/actions.py rename imagekit/imagecache/{backends/base.py => backends.py} (98%) delete mode 100644 imagekit/imagecache/backends/__init__.py delete mode 100644 imagekit/imagecache/backends/celery.py create mode 100644 imagekit/imagecache/strategies.py diff --git a/imagekit/conf.py b/imagekit/conf.py index 6c09232..e43d04e 100644 --- a/imagekit/conf.py +++ b/imagekit/conf.py @@ -1,9 +1,10 @@ from appconf import AppConf +from .imagecache.actions import validate_now, clear_now class ImageKitConf(AppConf): DEFAULT_IMAGE_CACHE_BACKEND = 'imagekit.imagecache.backends.Simple' CACHE_BACKEND = None - VALIDATE_ON_ACCESS = True CACHE_DIR = 'CACHE/images' CACHE_PREFIX = 'ik-' + DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY = 'imagekit.imagecache.strategies.Pessimistic' diff --git a/imagekit/generators.py b/imagekit/generators.py index 515bcb7..e848981 100644 --- a/imagekit/generators.py +++ b/imagekit/generators.py @@ -56,7 +56,7 @@ class SpecFileGenerator(object): self.format, pickle.dumps(self.options), str(self.autoconvert), - ])).hexdigest() + ]).encode('utf-8')).hexdigest() def generate_filename(self, source_file): source_filename = source_file.name diff --git a/imagekit/imagecache/actions.py b/imagekit/imagecache/actions.py new file mode 100644 index 0000000..11c0534 --- /dev/null +++ b/imagekit/imagecache/actions.py @@ -0,0 +1,23 @@ +def validate_now(file): + print 'validate now!' + file.validate() + + +try: + from celery.task import task +except ImportError: + pass +else: + validate_now_task = task(validate_now) + + +def deferred_validate(file): + try: + import celery + except: + raise ImportError("Deferred validation requires the the 'celery' library") + validate_now_task.delay(file) + + +def clear_now(file): + file.clear() diff --git a/imagekit/imagecache/backends/base.py b/imagekit/imagecache/backends.py similarity index 98% rename from imagekit/imagecache/backends/base.py rename to imagekit/imagecache/backends.py index a489f6d..cbb7f5d 100644 --- a/imagekit/imagecache/backends/base.py +++ b/imagekit/imagecache/backends.py @@ -1,4 +1,4 @@ -from ...utils import get_singleton +from ..utils import get_singleton from django.core.cache import get_cache from django.core.cache.backends.dummy import DummyCache from django.core.exceptions import ImproperlyConfigured diff --git a/imagekit/imagecache/backends/__init__.py b/imagekit/imagecache/backends/__init__.py deleted file mode 100644 index a733a93..0000000 --- a/imagekit/imagecache/backends/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .base import * -from .celery import * diff --git a/imagekit/imagecache/backends/celery.py b/imagekit/imagecache/backends/celery.py deleted file mode 100644 index f3c103d..0000000 --- a/imagekit/imagecache/backends/celery.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -from .base import InvalidImageCacheBackendError, Simple as SimpleBackend - - -def generate(model, pk, attr): - try: - instance = model._default_manager.get(pk=pk) - except model.DoesNotExist: - pass # The model was deleted since the task was scheduled. NEVER MIND! - else: - field_file = getattr(instance, attr) - field_file.delete(save=False) - field_file.generate(save=True) - - -class CeleryBackend(SimpleBackend): - """ - An image cache backend that uses celery to generate images. - - """ - def __init__(self): - try: - from celery.task import task - except: - raise InvalidImageCacheBackendError("Celery validation backend requires the 'celery' library") - if not getattr(CeleryBackend, '_task', None): - CeleryBackend._task = task(generate) - - def invalidate(self, file): - self._task.delay(file.instance.__class__, file.instance.pk, file.attname) - - def clear(self, file): - file.delete(save=False) diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py new file mode 100644 index 0000000..7ec7252 --- /dev/null +++ b/imagekit/imagecache/strategies.py @@ -0,0 +1,57 @@ +from .actions import validate_now, clear_now +from ..utils import get_singleton + + +class Pessimistic(object): + """ + A caching strategy that validates the file every time it's accessed. + + """ + + def on_access(self, file): + validate_now(file) + + def on_source_delete(self, file): + clear_now(file) + + def on_source_change(self, file): + validate_now(file) + + +class Optimistic(object): + """ + A caching strategy that validates when the source file changes and assumes + that the cached file will persist. + + """ + + def on_source_create(self, file): + validate_now(file) + + def on_source_delete(self, file): + clear_now(file) + + def on_source_change(self, file): + validate_now(file) + + +class DictStrategy(object): + def __init__(self, callbacks): + for k, v in callbacks.items(): + setattr(self, k, v) + + +class StrategyWrapper(object): + def __init__(self, strategy): + if isinstance(strategy, basestring): + strategy = get_singleton(strategy, 'image cache strategy') + elif isinstance(strategy, dict): + strategy = DictStrategy(strategy) + elif callable(strategy): + strategy = strategy() + self._wrapped = strategy + + def invoke_callback(self, name, *args, **kwargs): + func = getattr(self._wrapped, 'on_%s' % name, None) + if func: + func(*args, **kwargs) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 49278dc..fdb4475 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -5,6 +5,7 @@ from django.db import models from django.db.models.signals import post_init, post_save, post_delete from ...imagecache.backends import get_default_image_cache_backend +from ...imagecache.strategies import StrategyWrapper from ...generators import SpecFileGenerator from .files import ImageSpecFieldFile, ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor, ImageKitMeta, BoundImageKitMeta @@ -19,7 +20,7 @@ class ImageSpecField(object): """ def __init__(self, processors=None, format=None, options=None, image_field=None, pre_cache=None, storage=None, autoconvert=True, - image_cache_backend=None, validate_on_access=None): + image_cache_backend=None, image_cache_strategy=None): """ :param processors: A list of processors to run on the original image. :param format: The format of the output file. If not provided, @@ -38,9 +39,10 @@ class ImageSpecField(object): ``prepare_image()`` should be performed prior to saving. :param image_cache_backend: An object responsible for managing the state of cached files. Defaults to an instance of - IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND - :param validate_on_access: Should the image cache be validated when it's - accessed? + ``IMAGEKIT_DEFAULT_IMAGE_CACHE_BACKEND`` + :param image_cache_strategy: A dictionary containing callbacks that + allow you to customize how and when the image cache is validated. + Defaults to ``IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY`` """ @@ -61,8 +63,9 @@ class ImageSpecField(object): self.storage = storage self.image_cache_backend = image_cache_backend or \ get_default_image_cache_backend() - self.validate_on_access = settings.IMAGEKIT_VALIDATE_ON_ACCESS if \ - validate_on_access is None else validate_on_access + if image_cache_strategy is None: + image_cache_strategy = settings.IMAGEKIT_DEFAULT_SPEC_FIELD_IMAGE_CACHE_STRATEGY + self.image_cache_strategy = StrategyWrapper(image_cache_strategy) def contribute_to_class(self, cls, name): setattr(cls, name, ImageSpecFileDescriptor(self, name)) @@ -101,8 +104,11 @@ class ImageSpecField(object): old_hashes = instance._ik._source_hashes.copy() new_hashes = ImageSpecField._update_source_hashes(instance) for attname in instance._ik.spec_fields: - if old_hashes[attname] != new_hashes[attname]: - getattr(instance, attname).invalidate() + file = getattr(instance, attname) + if created: + file.field.image_cache_strategy.invoke_callback('source_create', file) + elif old_hashes[attname] != new_hashes[attname]: + file.field.image_cache_strategy.invoke_callback('source_change', file) @staticmethod def _update_source_hashes(instance): @@ -119,7 +125,7 @@ class ImageSpecField(object): @staticmethod def _post_delete_receiver(sender, instance=None, **kwargs): for spec_file in instance._ik.spec_files: - spec_file.clear() + spec_file.field.image_cache_strategy.invoke_callback('source_delete', spec_file) @staticmethod def _post_init_receiver(sender, instance, **kwargs): diff --git a/imagekit/models/fields/files.py b/imagekit/models/fields/files.py index 687295f..2d58845 100644 --- a/imagekit/models/fields/files.py +++ b/imagekit/models/fields/files.py @@ -34,8 +34,7 @@ class ImageSpecFieldFile(ImageFieldFile): def _require_file(self): if not self.source_file: raise ValueError("The '%s' attribute's image_field has no file associated with it." % self.attname) - elif self.field.validate_on_access: - self.validate() + self.field.image_cache_strategy.invoke_callback('access', self) def clear(self): return self.field.image_cache_backend.clear(self) From c8778b9cfb07eb1b955ff34a9fd185ffd155386b Mon Sep 17 00:00:00 2001 From: Matthew Tretter Date: Wed, 3 Oct 2012 22:31:45 -0400 Subject: [PATCH 8/8] Remove print statement --- imagekit/imagecache/actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/imagekit/imagecache/actions.py b/imagekit/imagecache/actions.py index 11c0534..2e222b3 100644 --- a/imagekit/imagecache/actions.py +++ b/imagekit/imagecache/actions.py @@ -1,5 +1,4 @@ def validate_now(file): - print 'validate now!' file.validate()