Django

Code

Ticket #5361: file-backends.diff

File file-backends.diff, 23.7 kB (added by Marty Alchin <gulopine@gamemusic.org>, 10 months ago)

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

  • django/db/models/base.py

    old new  
    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

    old new  
    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

    old new  
     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

    old new  
     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

    old new  
     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

    old new  
     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

    old new  
    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