Contents:

Django图片系统


906 浏览 5 years, 10 months

9 源码解析

版权声明: 转载请注明出处 http://www.codingsoho.com/

源码解析

ImageField (FileField)

ImageField从FileField继承而来,它的保存功能是在FileField里实现的,我们来看一下调用结构

  • FileField::pre_save
    • Field::pre_save
    • FieldFile::save
      • Storage::save
        • FileSystemStorage::save
class FileField(Field):
    def pre_save(self, model_instance, add):
        "Returns field's value just before saving."
        file = super(FileField, self).pre_save(model_instance, add)
        if file and not file._committed:
            # Commit the file to storage prior to saving the model
            file.save(file.name, file, save=False)
        return file

class Field(RegisterLookupMixin):
    def pre_save(self, model_instance, add):
        """
        Returns field's value just before saving.
        """
        return getattr(model_instance, self.attname)

FileField首先通过pre_save函数进行处理,如果有需要在save之前处理的需求的话,可以在这儿实现,如果文件没有commit, 接下来它会调用save函数。

class FieldFile(File):
    def save(self, name, content, save=True):
        name = self.field.generate_filename(self.instance, name)
#
        args, varargs, varkw, defaults = getargspec(self.storage.save)
        if 'max_length' in args:
            self.name = self.storage.save(name, content, max_length=self.field.max_length)                                       # (1)
        else:
            warnings.warn(
                'Backwards compatibility for storage backends without '
                'support for the `max_length` argument in '
                'Storage.save() will be removed in Django 1.10.',
                RemovedInDjango110Warning, stacklevel=2
            )
            self.name = self.storage.save(name, content)
#
        setattr(self.instance, self.field.name, self.name)
#
        # Update the filesize cache
        self._size = content.size
        self._committed = True
#
        # Save the object because it has changed, unless save is False
        if save:
            self.instance.save()
    save.alters_data = True
  • (1) self.storage.save该语句调用storage进行存储,如果不做处理,它会调用文件的默认文件系统存储。但是有的时候你如果想修改行为,或者用外部存储比如SAE之类的话,需要替换这个storage.
class Storage(object):
    def save(self, name, content, max_length=None):
        """
        Saves new content to the file specified by name. The content should be
        a proper File object or any python file-like object, ready to be read
        from the beginning.
        """
        # Get the proper name for the file, as it will actually be saved.
        if name is None:
            name = content.name

        if not hasattr(content, 'chunks'):
            content = File(content)

        args, varargs, varkw, defaults = getargspec(self.get_available_name)
        if 'max_length' in args:
            name = self.get_available_name(name, max_length=max_length)
        else:
            warnings.warn(
                'Backwards compatibility for storage backends without '
                'support for the `max_length` argument in '
                'Storage.get_available_name() will be removed in Django 1.10.',
                RemovedInDjango110Warning, stacklevel=2
            )
            name = self.get_available_name(name)

        name = self._save(name, content)                                            # (1)

        # Store filenames with forward slashes, even on Windows
        return force_text(name.replace('\\', '/'))
  • (1) name = self._save(name, content)这个真正完成文件系统的存储操作,默认FileSystemStorage的实现如下。如前面所说,如果你想修改为其他的存储方式的话,可以修改DEFAULT_FILE_STORAGE, 比如下面这个是SAE的存储文件系统
DEFAULT_FILE_STORAGE = 'sae.ext.django.storage.backend.Storage'
class FileSystemStorage(Storage):
   def _save(self, name, content):
        full_path = self.path(name)

        # Create any intermediate directories that do not exist.
        # Note that there is a race between os.path.exists and os.makedirs:
        # if os.makedirs fails with EEXIST, the directory was created
        # concurrently, and we can continue normally. Refs #16082.
        directory = os.path.dirname(full_path)
        if not os.path.exists(directory):
            try:
                if self.directory_permissions_mode is not None:
                    # os.makedirs applies the global umask, so we reset it,
                    # for consistency with file_permissions_mode behavior.
                    old_umask = os.umask(0)
                    try:
                        os.makedirs(directory, self.directory_permissions_mode)
                    finally:
                        os.umask(old_umask)
                else:
                    os.makedirs(directory)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
        if not os.path.isdir(directory):
            raise IOError("%s exists and is not a directory." % directory)

        # There's a potential race condition between get_available_name and
        # saving the file; it's possible that two threads might return the
        # same name, at which point all sorts of fun happens. So we need to
        # try to create the file, but if it already exists we have to go back
        # to get_available_name() and try again.

        while True:
            try:
                # This file has a file path that we can move.
                if hasattr(content, 'temporary_file_path'):
                    file_move_safe(content.temporary_file_path(), full_path)

                # This is a normal uploadedfile that we can stream.
                else:
                    # This fun binary flag incantation makes os.open throw an
                    # OSError if the file already exists before we open it.
                    flags = (os.O_WRONLY | os.O_CREAT | os.O_EXCL |
                             getattr(os, 'O_BINARY', 0))
                    # The current umask value is masked out by os.open!
                    fd = os.open(full_path, flags, 0o666)
                    _file = None
                    try:
                        locks.lock(fd, locks.LOCK_EX)
                        for chunk in content.chunks():
                            if _file is None:
                                mode = 'wb' if isinstance(chunk, bytes) else 'wt'
                                _file = os.fdopen(fd, mode)
                            _file.write(chunk)
                    finally:
                        locks.unlock(fd)
                        if _file is not None:
                            _file.close()
                        else:
                            os.close(fd)
            except OSError as e:
                if e.errno == errno.EEXIST:
                    # Ooops, the file exists. We need a new file name.
                    name = self.get_available_name(name)
                    full_path = self.path(name)
                else:
                    raise
            else:
                # OK, the file save worked. Break out of the loop.
                break

        if self.file_permissions_mode is not None:
            os.chmod(full_path, self.file_permissions_mode)

        return name

UploadFile

接下来看看文件系统的上传时怎么完成的

django\core\files\uploadedfile.py

字典request.FILES中的每一个条目都是一个UploadedFile对象。

UploadedFile对象有如下方法:

  1. UploadFile.read(): 从文件中读取全部上传数据。当上传文件过大时,可能会耗尽内存,慎用。
  2. UploadFile.multiple_chunks(): 如上传文件足够大,要分成多个部分读入时,返回True.默认情况,当上传文件大于2.5M时,返回True。但这一个值可以配置。
  3. UploadFile.chunks(): 返回一个上传文件的分块生成器。如multiple_chunks()返回True,必须在循环中使用chrunks()来代替read()。一般情况下直接使用chunks()就行。
  4. UploadFile.name():上传文件的文件名
  5. UplaodFile.size():上传文件的文件大小(字节)

由上面的说明可以写出handle_uploaded_file函数

def handle_uploaded_file(f):
  destination = open('some/file/name.txt', 'wb+')
  for chunk in f.chunks():
    destination.write(chunk)
  destination.close()

上传文件保存的位置

保存上传文件前,数据需要存放在某个位置。默认时,当上传文件小于2.5M时,django会将上传文件的全部内容读进内存。意味着保存文件只有一次从内存读取,一次写磁盘。 但当上传文件很大时,django会把上传文件写到临时文件中,然后存放到系统临时文件夹中。

改变upload handler的行为

三个设置控制django文件上传的行为:

FILE_UPLOAD_MAX_MEMORY_SIZE: 直接读入内存的最大上传文件大小(字节数)。当大于此值时,文件存放到磁盘。默认2.5M字节
FILE_UPLOAD_TEMP_DIR
FILE_UPLOAD_PERMISSIONS: 权限

FILE_UPLOAD_HANDLERS

上传文件真正的处理句柄。修改此项设置可以完成自定义django上传文件的过程。

默认是两面两个:

# List of upload handler classes to be applied in order.
FILE_UPLOAD_HANDLERS = (
    'django.core.files.uploadhandler.MemoryFileUploadHandler',
    'django.core.files.uploadhandler.TemporaryFileUploadHandler',
)