2012-10-17 03:38:44 +00:00
from django . conf import settings
2012-10-17 02:52:01 +00:00
from django . core . files . base import ContentFile , File
from django . core . files . images import ImageFile
2012-10-17 01:31:47 +00:00
from django . utils . encoding import smart_str , smart_unicode
2012-10-21 02:53:55 +00:00
from hashlib import md5
2012-10-17 01:31:47 +00:00
import os
2012-11-02 04:27:29 +00:00
import pickle
2012-10-17 02:30:36 +00:00
from . signals import before_access
2012-11-02 04:27:29 +00:00
from . utils import ( suggest_extension , format_to_mimetype , format_to_extension ,
2012-10-18 01:09:04 +00:00
extension_to_mimetype , get_logger , get_singleton )
2012-02-12 05:06:52 +00:00
2012-10-17 03:51:26 +00:00
class BaseIKFile ( File ) :
2012-10-17 02:52:01 +00:00
"""
This class contains all of the methods we need from
django . db . models . fields . files . FieldFile , but with the model stuff ripped
out . It ' s only extended by one class, but we keep it separate for
organizational reasons .
"""
2012-10-17 04:29:51 +00:00
def __init__ ( self , storage ) :
self . storage = storage
2012-10-17 02:52:01 +00:00
def _require_file ( self ) :
if not self :
raise ValueError ( )
def _get_file ( self ) :
self . _require_file ( )
if not hasattr ( self , ' _file ' ) or self . _file is None :
self . _file = self . storage . open ( self . name , ' rb ' )
return self . _file
def _set_file ( self , file ) :
self . _file = file
def _del_file ( self ) :
del self . _file
file = property ( _get_file , _set_file , _del_file )
def _get_path ( self ) :
self . _require_file ( )
return self . storage . path ( self . name )
path = property ( _get_path )
def _get_url ( self ) :
self . _require_file ( )
return self . storage . url ( self . name )
url = property ( _get_url )
def _get_size ( self ) :
self . _require_file ( )
if not self . _committed :
return self . file . size
return self . storage . size ( self . name )
size = property ( _get_size )
def open ( self , mode = ' rb ' ) :
self . _require_file ( )
self . file . open ( mode )
def _get_closed ( self ) :
file = getattr ( self , ' _file ' , None )
return file is None or file . closed
closed = property ( _get_closed )
def close ( self ) :
file = getattr ( self , ' _file ' , None )
if file is not None :
file . close ( )
2012-11-02 04:27:29 +00:00
class GeneratedImageCacheFile ( BaseIKFile , ImageFile ) :
"""
A cache file that represents the result of a generator . Creating an instance
of this class is not enough to trigger the creation of the cache file . In
fact , one of the main points of this class is to allow the creation of the
file to be deferred until the time that the image cache strategy requires
it .
2012-10-16 03:53:05 +00:00
2012-11-02 04:27:29 +00:00
"""
2012-11-03 02:17:25 +00:00
def __init__ ( self , generator , name = None , * args , * * kwargs ) :
2012-11-02 04:27:29 +00:00
"""
: param generator : The object responsible for generating a new image .
: param args : Positional arguments that will be passed to the generator ' s
` ` generate ( ) ` ` method when the generation is called for .
: param kwargs : Keyword arguments that will be apssed to the generator ' s
` ` generate ( ) ` ` method when the generation is called for .
"""
2012-11-03 02:17:25 +00:00
self . _name = name
2012-11-02 04:27:29 +00:00
self . generator = generator
self . args = args
self . kwargs = kwargs
storage = getattr ( generator , ' storage ' , None )
if not storage and settings . IMAGEKIT_DEFAULT_FILE_STORAGE :
storage = get_singleton ( settings . IMAGEKIT_DEFAULT_FILE_STORAGE ,
' file storage backend ' )
super ( GeneratedImageCacheFile , self ) . __init__ ( storage = storage )
2012-02-12 05:06:52 +00:00
2012-11-03 02:17:25 +00:00
def get_default_filename ( self ) :
2012-11-02 04:27:29 +00:00
# FIXME: This won't work if args or kwargs contain a file object. It probably won't work in many other cases as well. Better option?
hash = md5 ( ' ' . join ( [
pickle . dumps ( self . args ) ,
pickle . dumps ( self . kwargs ) ,
self . generator . get_hash ( ) ,
] ) . encode ( ' utf-8 ' ) ) . hexdigest ( )
ext = format_to_extension ( self . generator . format )
return os . path . join ( settings . IMAGEKIT_CACHE_DIR ,
' %s %s ' % ( hash , ext ) )
2012-11-03 02:17:25 +00:00
def _get_name ( self ) :
return self . _name or self . get_default_filename ( )
def _set_name ( self , value ) :
self . _name = value
name = property ( _get_name , _set_name )
2012-11-02 04:27:29 +00:00
def _require_file ( self ) :
before_access . send ( sender = self , generator = self . generator , file = self )
return super ( GeneratedImageCacheFile , self ) . _require_file ( )
2012-02-12 05:06:52 +00:00
2012-10-17 02:33:17 +00:00
def clear ( self ) :
2012-11-02 04:27:29 +00:00
return self . generator . image_cache_backend . clear ( self )
2012-10-17 02:33:17 +00:00
def invalidate ( self ) :
2012-11-02 04:27:29 +00:00
return self . generator . image_cache_backend . invalidate ( self )
2012-10-17 02:33:17 +00:00
def validate ( self ) :
2012-11-02 04:27:29 +00:00
return self . generator . image_cache_backend . validate ( self )
2012-10-17 02:33:17 +00:00
2012-10-17 04:31:38 +00:00
def generate ( self ) :
2012-11-02 04:27:29 +00:00
# Generate the file
content = self . generator . generate ( * self . args , * * self . kwargs )
actual_name = self . storage . save ( self . name , content )
if actual_name != self . name :
get_logger ( ) . warning ( ' The storage backend %s did not save the file '
' with the requested name ( " %s " ) and instead used '
' " %s " . This may be because a file already existed with '
' the requested name. If so, you may have meant to call '
' validate() instead of generate(), or there may be a '
' race condition in the image cache backend %s . The '
' saved file will not be used. ' % ( self . storage ,
self . name , actual_name ,
self . generator . image_cache_backend ) )
class ImageSpecCacheFile ( GeneratedImageCacheFile ) :
def __init__ ( self , generator , source_file ) :
super ( ImageSpecCacheFile , self ) . __init__ ( generator ,
source_file = source_file )
if not self . storage :
self . storage = source_file . storage
2012-11-03 02:17:25 +00:00
def get_default_filename ( self ) :
2012-11-02 04:27:29 +00:00
source_filename = self . kwargs [ ' source_file ' ] . name
hash = md5 ( ' ' . join ( [
source_filename ,
self . generator . get_hash ( ) ,
] ) . encode ( ' utf-8 ' ) ) . hexdigest ( )
# TODO: Since specs can now be dynamically generated using hints, can we move this into the spec constructor? i.e. set self.format if not defined. This would get us closer to making ImageSpecCacheFile == GeneratedImageCacheFile
ext = suggest_extension ( source_filename , self . generator . format )
return os . path . normpath ( os . path . join (
settings . IMAGEKIT_CACHE_DIR ,
os . path . splitext ( source_filename ) [ 0 ] ,
' %s %s ' % ( hash , ext ) ) )
2012-10-17 01:31:47 +00:00
class IKContentFile ( ContentFile ) :
"""
Wraps a ContentFile in a file - like object with a filename and a
content_type . A PIL image format can be optionally be provided as a content
type hint .
"""
def __init__ ( self , filename , content , format = None ) :
self . file = ContentFile ( content )
self . file . name = filename
mimetype = getattr ( self . file , ' content_type ' , None )
if format and not mimetype :
mimetype = format_to_mimetype ( format )
if not mimetype :
ext = os . path . splitext ( filename or ' ' ) [ 1 ]
mimetype = extension_to_mimetype ( ext )
self . file . content_type = mimetype
2012-10-26 02:43:10 +00:00
@property
def name ( self ) :
return self . file . name
2012-10-17 01:31:47 +00:00
def __str__ ( self ) :
return smart_str ( self . file . name or ' ' )
def __unicode__ ( self ) :
return smart_unicode ( self . file . name or u ' ' )