Ticket #5361: file-backends.diff

File file-backends.diff, 23.7 KB (added by Marty Alchin <gulopine@…>, 12 years ago)

FileField refactor to support backends, as well as a FileSystemBackend to implement existing functionality

  • django/db/models/base.py

     
    1818import types
    1919import sys
    2020import os
     21from warnings import warn
    2122
    2223class ModelBase(type):
    2324    "Metaclass for all models"
     
    357358        return getattr(self, cachename)
    358359
    359360    def _get_FIELD_filename(self, field):
    360         if getattr(self, field.attname): # value is not blank
    361             return os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname))
    362         return ''
     361        warn("Use instance.%s.open() if you need access to the file." % field.attname, DeprecationWarning)
     362        return field.backend._get_absolute_path(self.__dict__[field.attname])
    363363
    364364    def _get_FIELD_url(self, field):
    365         if getattr(self, field.attname): # value is not blank
    366             import urlparse
    367             return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/')
    368         return ''
     365        warn("Use instance.%s.get_absolute_url()." % field.attname, DeprecationWarning)
     366        return getattr(self, field.attname).get_absolute_url()
    369367
    370368    def _get_FIELD_size(self, field):
    371         return os.path.getsize(self._get_FIELD_filename(field))
     369        warn("Use instance.%s.get_filesize()." % field.attname, DeprecationWarning)
     370        return getattr(self, field.attname).get_filesize()
    372371
    373372    def _save_FIELD_file(self, field, filename, raw_contents, save=True):
    374         directory = field.get_directory_name()
    375         try: # Create the date-based directory if it doesn't exist.
    376             os.makedirs(os.path.join(settings.MEDIA_ROOT, directory))
    377         except OSError: # Directory probably already exists.
    378             pass
    379         filename = field.get_filename(filename)
     373        warn("Use instance.%s.save_file()." % field.attname, DeprecationWarning)
     374        return getattr(self, field.attname).save_file(filename, raw_contents, save)
    380375
    381         # If the filename already exists, keep adding an underscore to the name of
    382         # the file until the filename doesn't exist.
    383         while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)):
    384             try:
    385                 dot_index = filename.rindex('.')
    386             except ValueError: # filename has no dot
    387                 filename += '_'
    388             else:
    389                 filename = filename[:dot_index] + '_' + filename[dot_index:]
    390 
    391         # Write the file to disk.
    392         setattr(self, field.attname, filename)
    393 
    394         full_filename = self._get_FIELD_filename(field)
    395         fp = open(full_filename, 'wb')
    396         fp.write(raw_contents)
    397         fp.close()
    398 
    399         # Save the width and/or height, if applicable.
    400         if isinstance(field, ImageField) and (field.width_field or field.height_field):
    401             from django.utils.images import get_image_dimensions
    402             width, height = get_image_dimensions(full_filename)
    403             if field.width_field:
    404                 setattr(self, field.width_field, width)
    405             if field.height_field:
    406                 setattr(self, field.height_field, height)
    407 
    408         # Save the object because it has changed unless save is False
    409         if save:
    410             self.save()
    411 
    412     _save_FIELD_file.alters_data = True
    413 
    414376    def _get_FIELD_width(self, field):
    415         return self._get_image_dimensions(field)[0]
     377        warn("Use instance.%s.get_width()." % field.attname, DeprecationWarning)
     378        return getattr(self, field.attname).get_width()
    416379
    417380    def _get_FIELD_height(self, field):
    418         return self._get_image_dimensions(field)[1]
     381        warn("Use instance.%s.get_height()." % field.attname, DeprecationWarning)
     382        return getattr(self, field.attname).get_height()
    419383
    420     def _get_image_dimensions(self, field):
    421         cachename = "__%s_dimensions_cache" % field.name
    422         if not hasattr(self, cachename):
    423             from django.utils.images import get_image_dimensions
    424             filename = self._get_FIELD_filename(field)
    425             setattr(self, cachename, get_image_dimensions(filename))
    426         return getattr(self, cachename)
    427 
    428384############################################
    429385# HELPER FUNCTIONS (CURRIED MODEL METHODS) #
    430386############################################
  • django/db/models/fields/__init__.py

     
    694694        defaults.update(kwargs)
    695695        return super(EmailField, self).formfield(**defaults)
    696696
     697class File(object):
     698    def __init__(self, obj, field, filename):
     699        self.obj = obj
     700        self.field = field
     701        self.backend = field.backend
     702        self.filename = filename
     703
     704    def __str__(self):
     705        return self.backend.get_filename(self.filename)
     706
     707    def get_absolute_url(self):
     708        return self.backend.get_absolute_url(self.filename)
     709
     710    def get_filesize(self):
     711        return self.backend.get_filesize(self.filename)
     712
     713    def open(self, mode='rb'):
     714        return self.backend.open(self.filename, mode)
     715
     716    def save_file(self, filename, raw_contents, save=True):
     717        self.filename = self.backend.save_file(filename, raw_contents)
     718
     719        # Save the object because it has changed unless save is False
     720        if save:
     721            self.obj.save()
     722
     723class FileProxy(object):
     724    def __init__(self, field):
     725        self.field = field
     726        self.cache_name = self.field.get_cache_name()
     727
     728    def __get__(self, instance=None, owner=None):
     729        if instance is None:
     730            raise AttributeError, "%s can only be accessed from %s instances." % (self.field.attname, self.owner.__name__)
     731        return getattr(instance, self.cache_name)
     732
     733    def __set__(self, instance, value):
     734        if hasattr(instance, self.cache_name):
     735            raise AttributeError, "%s can not be set in this manner." % self.field.attname
     736        instance.__dict__[self.field.attname] = value
     737        attr = self.field.attr_class(instance, self.field, value)
     738        setattr(instance, self.cache_name, attr)
     739
    697740class FileField(Field):
    698     def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs):
    699         self.upload_to = upload_to
     741    attr_class = File
     742
     743    def __init__(self, verbose_name=None, name=None, upload_to='', backend=None, **kwargs):
     744        if backend is None:
     745            from django.db.models.fields.backends.filesystem import FileSystemBackend
     746            backend = FileSystemBackend(location=upload_to)
     747        self.backend = self.upload_to = backend
    700748        Field.__init__(self, verbose_name, name, **kwargs)
    701749
    702750    def get_db_prep_save(self, value):
     
    704752        # Need to convert UploadedFile objects provided via a form to unicode for database insertion
    705753        if value is None:
    706754            return None
    707         return unicode(value)
     755        return unicode(value.filename)
    708756
    709757    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
    710758        field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
     
    744792
    745793    def contribute_to_class(self, cls, name):
    746794        super(FileField, self).contribute_to_class(cls, name)
     795        setattr(cls, self.attname, FileProxy(self))
    747796        setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
    748797        setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
    749798        setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
    750799        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save))
    751800        dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)
    752801
    753     def delete_file(self, instance):
    754         if getattr(instance, self.attname):
    755             file_name = getattr(instance, 'get_%s_filename' % self.name)()
    756             # If the file exists and no other object of this type references it,
    757             # delete it from the filesystem.
    758             if os.path.exists(file_name) and \
    759                 not instance.__class__._default_manager.filter(**{'%s__exact' % self.name: getattr(instance, self.attname)}):
    760                 os.remove(file_name)
     802    def delete_file(self, instance, sender):
     803        filename = getattr(instance, self.attname).filename
     804        # If no other object of this type references the file,
     805        # delete it from the backend.
     806        if not sender._default_manager.filter(**{self.name: filename}):
     807            self.backend.delete_file(filename)
    761808
    762809    def get_manipulator_field_objs(self):
    763810        return [oldforms.FileUploadField, oldforms.HiddenField]
     
    768815    def save_file(self, new_data, new_object, original_object, change, rel, save=True):
    769816        upload_field_name = self.get_manipulator_field_names('')[0]
    770817        if new_data.get(upload_field_name, False):
    771             func = getattr(new_object, 'save_%s_file' % self.name)
    772818            if rel:
    773                 func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"], save)
     819                field = new_data[upload_field_name][0]
    774820            else:
    775                 func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"], save)
     821                field = new_data[upload_field_name]
     822            getattr(new_object, self.attname).save_file(field["filename"], field["content"], save)
    776823
    777     def get_directory_name(self):
    778         return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
    779 
    780     def get_filename(self, filename):
    781         from django.utils.text import get_valid_filename
    782         f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
    783         return os.path.normpath(f)
    784 
    785824    def save_form_data(self, instance, data):
    786825        if data:
    787             getattr(instance, "save_%s_file" % self.name)(data.filename, data.content, save=False)
    788        
     826            getattr(instance, self.attnamename).save_file(data.filename, data.content, save=False)
     827
    789828    def formfield(self, **kwargs):
    790829        defaults = {'form_class': forms.FileField}
    791830        # If a file has been provided previously, then the form doesn't require
     
    814853        defaults.update(kwargs)
    815854        return super(FloatField, self).formfield(**defaults)
    816855
     856class ImageFile(File):
     857    def get_width(self):
     858        return self._get_image_dimensions()[0]
     859
     860    def get_height(self):
     861        return self._get_image_dimensions()[1]
     862
     863    def _get_image_dimensions(self):
     864        if not hasattr(self, '_dimensions_cache'):
     865            from django.utils.images import get_image_dimensions
     866            self._dimensions_cache = get_image_dimensions(self.open())
     867        return self._dimensions_cache
     868
    817869class ImageField(FileField):
     870    attr_class = ImageFile
     871
    818872    def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
    819873        self.width_field, self.height_field = width_field, height_field
    820874        FileField.__init__(self, verbose_name, name, **kwargs)
     
    832886            setattr(cls, 'get_%s_height' % self.name, curry(cls._get_FIELD_height, field=self))
    833887
    834888    def save_file(self, new_data, new_object, original_object, change, rel, save=True):
    835         FileField.save_file(self, new_data, new_object, original_object, change, rel, save)
    836889        # If the image has height and/or width field(s) and they haven't
    837890        # changed, set the width and/or height field(s) back to their original
    838891        # values.
    839         if change and (self.width_field or self.height_field) and save:
    840             if self.width_field:
    841                 setattr(new_object, self.width_field, getattr(original_object, self.width_field))
    842             if self.height_field:
    843                 setattr(new_object, self.height_field, getattr(original_object, self.height_field))
    844             new_object.save()
     892        if self.width_field or self.height_field:
     893            if original_object and not change:
     894                if self.width_field:
     895                    setattr(new_object, self.width_field, getattr(original_object, self.width_field))
     896                if self.height_field:
     897                    setattr(new_object, self.height_field, getattr(original_object, self.height_field))
     898            else:
     899                from cStringIO import StringIO
     900                from django.utils.images import get_image_dimensions
    845901
     902                upload_field_name = self.get_manipulator_field_names('')[0]
     903                if rel:
     904                    field = new_data[upload_field_name][0]
     905                else:
     906                    field = new_data[upload_field_name]
     907
     908                # Get the width and height from the raw content to avoid extra
     909                # unnecessary trips to the file backend.
     910                width, height = get_image_dimensions(StringIO(field["content"]))
     911
     912                if self.width_field:
     913                    setattr(new_object, self.width_field, width)
     914                if self.height_field:
     915                    setattr(new_object, self.height_field, height)
     916        FileField.save_file(self, new_data, new_object, original_object, change, rel, save)
     917
    846918    def formfield(self, **kwargs):
    847919        defaults = {'form_class': forms.ImageField}
    848920        return super(ImageField, self).formfield(**defaults)
  • django/db/models/fields/backends/__init__.py

     
     1from StringIO import StringIO
     2
     3class Backend(object):
     4    def get_available_filename(self, filename):
     5        # If the filename already exists, keep adding an underscore to the name
     6        # of the file until the filename doesn't exist.
     7        while self.file_exists(filename):
     8            try:
     9                dot_index = filename.rindex('.')
     10            except ValueError: # filename has no dot
     11                filename += '_'
     12            else:
     13                filename = filename[:dot_index] + '_' + filename[dot_index:]
     14        return filename
     15
     16    def get_filename(self, filename):
     17        return filename
     18
     19class RemoteFile(StringIO):
     20    """Sends files to a remote backend automatically, when necessary."""
     21
     22    def __init__(self, data, mode, writer):
     23        self._mode = mode
     24        self._write_to_backend = writer
     25        self._is_dirty = False
     26        StringIO.__init__(self, data)
     27
     28    def write(self, data):
     29        if 'w' not in self._mode:
     30            raise AttributeError, "File was opened for read-only access."
     31        StringIO.write(self, data)
     32        self._is_dirty = True
     33
     34    def close(self):
     35        if self._is_dirty:
     36            self._write_to_backend(self.getvalue())
     37        StringIO.close(self)
     38
  • django/db/models/fields/backends/filesystem.py

     
     1import datetime
     2import os
     3
     4from django.conf import settings
     5from django.utils.encoding import force_unicode, smart_str
     6
     7from django.db.models.fields.backends import Backend
     8
     9class FileSystemBackend(Backend):
     10    """Standard filesystem storage"""
     11
     12    def __init__(self, location='', media_root=None, media_url=None):
     13        self.location = location
     14        if media_root != None and media_url != None:
     15            # Both were provided, so use them
     16            pass
     17        elif media_root is None and media_url is None:
     18            # Neither were provided, so use global settings
     19            from django.conf import settings
     20            try:
     21                media_root = settings.MEDIA_ROOT
     22                media_url = settings.MEDIA_URL
     23            except AttributeError:
     24                raise ImproperlyConfigured, "Media settings not defined."
     25        else:
     26            # One or the other were provided, but not both
     27            raise ImproperlyConfigured, "Both media_root and media_url must be provided."
     28        self.media_root = media_root
     29        self.media_url = media_url
     30
     31    def _get_directory_name(self):
     32        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.location))))
     33
     34    def _get_absolute_path(self, filename):
     35        return os.path.normpath(os.path.join(self.media_root, filename))
     36
     37    # The following methods define the Backend API
     38
     39    def get_available_filename(self, filename):
     40        from django.utils.text import get_valid_filename
     41        f = os.path.join(self._get_directory_name(), get_valid_filename(os.path.basename(filename)))
     42        return Backend.get_available_filename(self, os.path.normpath(f))
     43
     44    def get_filesize(self, filename):
     45        return os.path.getsize(self._get_absolute_path(filename))
     46
     47    def get_absolute_url(self, filename):
     48        import urlparse
     49        return urlparse.urljoin(self.media_url, filename).replace('\\', '/')
     50
     51    def open(self, filename, mode='rb'):
     52        return open(self._get_absolute_path(filename), mode)
     53
     54    def file_exists(self, filename):
     55        return os.path.exists(self._get_absolute_path(filename))
     56
     57    def save_file(self, filename, raw_contents):
     58        directory = self._get_directory_name()
     59        try: # Create the date-based directory if it doesn't exist.
     60            os.makedirs(os.path.join(self.media_root, directory))
     61        except OSError: # Directory probably already exists.
     62            pass
     63        filename = self.get_available_filename(filename)
     64
     65        # Write the file to disk.
     66        fp = open(self._get_absolute_path(filename), 'wb')
     67        fp.write(raw_contents)
     68        fp.close()
     69
     70        return filename
     71
     72    def delete_file(self, filename):
     73        file_name = self._get_absolute_path(filename)
     74        # If the file exists, delete it from the filesystem.
     75        if os.path.exists(file_name):
     76            os.remove(file_name)
  • django/db/models/fields/backends/__init__.py

     
     1from StringIO import StringIO
     2
     3class Backend(object):
     4    def get_available_filename(self, filename):
     5        # If the filename already exists, keep adding an underscore to the name
     6        # of the file until the filename doesn't exist.
     7        while self.file_exists(filename):
     8            try:
     9                dot_index = filename.rindex('.')
     10            except ValueError: # filename has no dot
     11                filename += '_'
     12            else:
     13                filename = filename[:dot_index] + '_' + filename[dot_index:]
     14        return filename
     15
     16    def get_filename(self, filename):
     17        return filename
     18
     19class RemoteFile(StringIO):
     20    """Sends files to a remote backend automatically, when necessary."""
     21
     22    def __init__(self, data, mode, writer):
     23        self._mode = mode
     24        self._write_to_backend = writer
     25        self._is_dirty = False
     26        StringIO.__init__(self, data)
     27
     28    def write(self, data):
     29        if 'w' not in self._mode:
     30            raise AttributeError, "File was opened for read-only access."
     31        StringIO.write(self, data)
     32        self._is_dirty = True
     33
     34    def close(self):
     35        if self._is_dirty:
     36            self._write_to_backend(self.getvalue())
     37        StringIO.close(self)
     38
  • django/db/models/fields/backends/filesystem.py

     
     1import datetime
     2import os
     3
     4from django.conf import settings
     5from django.utils.encoding import force_unicode, smart_str
     6
     7from django.db.models.fields.backends import Backend
     8
     9class FileSystemBackend(Backend):
     10    """Standard filesystem storage"""
     11
     12    def __init__(self, location='', media_root=None, media_url=None):
     13        self.location = location
     14        if media_root != None and media_url != None:
     15            # Both were provided, so use them
     16            pass
     17        elif media_root is None and media_url is None:
     18            # Neither were provided, so use global settings
     19            from django.conf import settings
     20            try:
     21                media_root = settings.MEDIA_ROOT
     22                media_url = settings.MEDIA_URL
     23            except AttributeError:
     24                raise ImproperlyConfigured, "Media settings not defined."
     25        else:
     26            # One or the other were provided, but not both
     27            raise ImproperlyConfigured, "Both media_root and media_url must be provided."
     28        self.media_root = media_root
     29        self.media_url = media_url
     30
     31    def _get_directory_name(self):
     32        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.location))))
     33
     34    def _get_absolute_path(self, filename):
     35        return os.path.normpath(os.path.join(self.media_root, filename))
     36
     37    # The following methods define the Backend API
     38
     39    def get_available_filename(self, filename):
     40        from django.utils.text import get_valid_filename
     41        f = os.path.join(self._get_directory_name(), get_valid_filename(os.path.basename(filename)))
     42        return Backend.get_available_filename(self, os.path.normpath(f))
     43
     44    def get_filesize(self, filename):
     45        return os.path.getsize(self._get_absolute_path(filename))
     46
     47    def get_absolute_url(self, filename):
     48        import urlparse
     49        return urlparse.urljoin(self.media_url, filename).replace('\\', '/')
     50
     51    def open(self, filename, mode='rb'):
     52        return open(self._get_absolute_path(filename), mode)
     53
     54    def file_exists(self, filename):
     55        return os.path.exists(self._get_absolute_path(filename))
     56
     57    def save_file(self, filename, raw_contents):
     58        directory = self._get_directory_name()
     59        try: # Create the date-based directory if it doesn't exist.
     60            os.makedirs(os.path.join(self.media_root, directory))
     61        except OSError: # Directory probably already exists.
     62            pass
     63        filename = self.get_available_filename(filename)
     64
     65        # Write the file to disk.
     66        fp = open(self._get_absolute_path(filename), 'wb')
     67        fp.write(raw_contents)
     68        fp.close()
     69
     70        return filename
     71
     72    def delete_file(self, filename):
     73        file_name = self._get_absolute_path(filename)
     74        # If the file exists, delete it from the filesystem.
     75        if os.path.exists(file_name):
     76            os.remove(file_name)
  • django/utils/images.py

     
    66
    77import ImageFile
    88
    9 def get_image_dimensions(path):
    10     """Returns the (width, height) of an image at a given path."""
     9def get_image_dimensions(file_or_path):
     10    """Returns the (width, height) of an image, given an open file or a path."""
    1111    p = ImageFile.Parser()
    12     fp = open(path, 'rb')
     12    if hasattr(file_or_path, 'read'):
     13        fp = file_or_path
     14    else:
     15        fp = open(file_or_path, 'rb')
    1316    while 1:
    1417        data = fp.read(1024)
    1518        if not data:
     
    1922            return p.image.size
    2023            break
    2124    fp.close()
    22     return None
     25    return None
     26 No newline at end of file
Back to Top