django 系列文章 之 默认文件系统和文件上传

一、django默认文件存储系统
几个模块或者类:
django.core.files 模块及其子模块包含了 Django 中基本的文件处理的内置类,如下图所示:


1、File类 和 ContentFile 类

File类:
在django中的位置:
from django.core.files.base import File

在Django中,File类是所有文件存储类的基类,是对 Python file object 的一个简单的封装,并增加了一些 Django 特有的功能。这个类主要用于与存储后端交互,如数据库、文件系统或者云存储服务等,提供了统一的API来操作文件。

主要属性和方法

  • name: 字符串,表示文件的名称,包括路径。
  • size: 整数,表示文件的大小(以字节为单位)。
  • open(mode='rb'): 方法,用于打开文件。默认以二进制读取模式打开,也支持其他模式如'wb'(写入)。
  • close(): 方法,关闭已经打开的文件。
  • read(num_bytes=None): 读取文件内容。如果不指定num_bytes,则读取整个文件。
  • write(content): 将内容写入文件。
  • chunks(chunk_size=None): 返回一个迭代器,用于分块读取大文件内容。
注意:
在django中操作文件时可以直接使用python 内置的操作文件的方法,也可以使用django的File类提供的方法,例如:
点击查看代码
# 使用python内置的操作文件的方法
f = open("/path/to/hello.world", 'r')
f.read()   # 调用的python内置的read()方法

# 使用django的File对象提供的方法
f = open("/path/to/hello.world", 'r')
myfile = File(f)
myfile.read()   # 调用django中File对象的read()方法
ContentFile 类:
在django中的位置:
from django.core.files.base import ContentFile

ContentFile 类继承自 File,但与 File 不同的是,它操作的是字符串内容(也支持字节),而不是实际的文件。例如:

点击查看代码
from django.core.files.base import ContentFile

f1 = ContentFile("esta frase está en español")
f2 = ContentFile(b"these are bytes")
File类 和 ContentFile 类 源码
点击查看代码

class File(FileProxyMixin):
    DEFAULT_CHUNK_SIZE = 64 * 2 ** 10

    def __init__(self, file, name=None):
        self.file = file
        if name is None:
            name = getattr(file, 'name', None)
        self.name = name
        if hasattr(file, 'mode'):
            self.mode = file.mode

    def __str__(self):
        return self.name or ''

    def __repr__(self):
        return "<%s: %s>" % (self.__class__.__name__, self or "None")

    def __bool__(self):
        return bool(self.name)

    def __len__(self):
        return self.size

    @cached_property
    def size(self):
        if hasattr(self.file, 'size'):
            return self.file.size
        if hasattr(self.file, 'name'):
            try:
                return os.path.getsize(self.file.name)
            except (OSError, TypeError):
                pass
        if hasattr(self.file, 'tell') and hasattr(self.file, 'seek'):
            pos = self.file.tell()
            self.file.seek(0, os.SEEK_END)
            size = self.file.tell()
            self.file.seek(pos)
            return size
        raise AttributeError("Unable to determine the file's size.")

    def chunks(self, chunk_size=None):
        """
        Read the file and yield chunks of ``chunk_size`` bytes (defaults to
        ``File.DEFAULT_CHUNK_SIZE``).
        """
        chunk_size = chunk_size or self.DEFAULT_CHUNK_SIZE
        try:
            self.seek(0)
        except (AttributeError, UnsupportedOperation):
            pass

        while True:
            data = self.read(chunk_size)
            if not data:
                break
            yield data

    def multiple_chunks(self, chunk_size=None):
        """
        Return ``True`` if you can expect multiple chunks.

        NB: If a particular file representation is in memory, subclasses should
        always return ``False`` -- there's no good reason to read from memory in
        chunks.
        """
        return self.size > (chunk_size or self.DEFAULT_CHUNK_SIZE)

    def __iter__(self):
        # Iterate over this file-like object by newlines
        buffer_ = None
        for chunk in self.chunks():
            for line in chunk.splitlines(True):
                if buffer_:
                    if endswith_cr(buffer_) and not equals_lf(line):
                        # Line split after a \r newline; yield buffer_.
                        yield buffer_
                        # Continue with line.
                    else:
                        # Line either split without a newline (line
                        # continues after buffer_) or with \r\n
                        # newline (line == b'\n').
                        line = buffer_ + line
                    # buffer_ handled, clear it.
                    buffer_ = None

                # If this is the end of a \n or \r\n line, yield.
                if endswith_lf(line):
                    yield line
                else:
                    buffer_ = line

        if buffer_ is not None:
            yield buffer_

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.close()

    def open(self, mode=None):
        if not self.closed:
            self.seek(0)
        elif self.name and os.path.exists(self.name):
            self.file = open(self.name, mode or self.mode)
        else:
            raise ValueError("The file cannot be reopened.")
        return self

    def close(self):
        self.file.close()


class ContentFile(File):
    """
    A File-like object that takes just raw content, rather than an actual file.
    """
    def __init__(self, content, name=None):
        stream_class = StringIO if isinstance(content, str) else BytesIO
        super().__init__(stream_class(content), name=name)
        self.size = len(content)

    def __str__(self):
        return 'Raw content'

    def __bool__(self):
        return True

    def open(self, mode=None):
        self.seek(0)
        return self

    def close(self):
        pass

    def write(self, data):
        self.__dict__.pop('size', None)  # Clear the computed size.
        return self.file.write(data)


def endswith_cr(line):
    """Return True if line (a text or bytestring) ends with '\r'."""
    return line.endswith('\r' if isinstance(line, str) else b'\r')


def endswith_lf(line):
    """Return True if line (a text or bytestring) ends with '\n'."""
    return line.endswith('\n' if isinstance(line, str) else b'\n')


def equals_lf(line):
    """Return True if line (a text or bytestring) equals '\n'."""
    return line == ('\n' if isinstance(line, str) else b'\n')


2、UploadedFile类和InMemoryUploadedFile类

UploadedFile类
在django中的位置:
from django.core.files.uploadedfile import UploadedFile

在文件上传过程中,实际的文件数据存储在 request.FILES 中。这个字典中的每一个条目都是一个 UploadedFile 对象(或一个子类对象)。该对象中包含一些常用的方法和属性:

点击查看代码
方法:
1read()
# 从文件中读取整个上传的数据。小心使用这个方法:如果上传的文件很大,如果你试图把它读到内存中,它可能会让你的系统不堪重负。

2multiple_chunks(chunk_size=None)
如果上传的文件足够大,需要分块读取,返回 True。默认情况下是大于 2.5MB的文件就需要分块。

3chunks(chunk_size=None)
返回一个生成器,用于逐块读取大文件内容,避免内存溢出。如果 multiple_chunks() 是 True,你应该在循环中使用这个方法而不是 read()。

在实践中,通常最简单的做法是使用 chunks() 而不是使用 read() ,这可以确保大文件不会过度占用系统的内存。

属性:
1、name
上传的文件名称(如 my_file.txt2、size
上传文件的大小,以字节为单位。

3、content_type_extra
包含传递给 content-type 头的额外参数的字典。

4、charset
对于 text/* 内容类型,浏览器提供的字符集(即 utf8)。
InMemoryUploadedFile类
在django中的位置:
from django.core.files.uploadedfile import UploadedFile,InMemoryUploadedFile

InMemoryUploadedFile 类用于处理那些大小适中、可以直接加载到内存中的上传文件。它是 UploadedFile 类的一个子类,专门为处理内存中的文件而设计, 常用的属性和方法如下:

点击查看代码
.read(size=-1): 读取文件内容。size 参数指定要读取的字节数,如果省略或为负,则读取直到文件结束。
.seek(offset, whence=0): 移动读取指针到文件中的某个位置。类似于普通文件操作。
.name: 文件的原始名称。
.size: 文件的大小(以字节为单位)。
.chunks(chunk_size=None): 即使是 InMemoryUploadedFile,也提供了这个方法,但它通常直接读取整个内容,除非你显式指定分块大小来迭代内容。

示例用法:

点击查看代码
def upload_view(request):
    if request.method == 'POST':
        uploaded_file = request.FILES['my_file']
        if isinstance(uploaded_file, InMemoryUploadedFile):
            # 文件较小,存储在内存中
            file_content = uploaded_file.read()
  
            # 进一步处理文件...


3、文件存储类FileSystemStorage

在django中位置:
django.core.files.storage.FileSystemStorage

FileSystemStorage 类是用于处理文件存储的类,它实现了将文件保存到本地文件系统的存储策略。这是Django提供的默认文件存储后端,允许开发者轻松地将上传的文件保存到服务器的硬盘上,并提供了一系列方法来管理这些文件。

主要特点和功能:
1. 本地存储:FileSystemStorage 将文件存储在由 MEDIA_ROOT 设置指定的目录下。每个上传的文件都会根据其内容生成一个唯一的文件名,并保存在相应的子目录结构中,以避免文件名冲突。

2.文件名处理:它提供了 get_available_name(name) 方法来处理文件名冲突。如果指定的文件名已经存在,该方法会自动修改文件名以保证唯一性,例如,通过添加序号(如 _1, _2 等)。

3.路径和URL:可以使用 path(name) 方法获取文件的绝对路径,以及 url(name) 方法获取文件的URL,以便在网页上引用。

4.文件访问控制:允许设置文件的权限模式(通过 file_permissions_mode 和 directory_permissions_mode 属性),以控制文件和目录的访问权限。

常用属性和方法:
点击查看代码
# 属性
location: 字符串,表示文件存储的基础目录路径。默认情况下,这将是 MEDIA_ROOT 的值。
base_url: 字符串,表示文件的公共URL前缀。默认情况下,这将是 MEDIA_URL 的值。
file_permissions_mode: 八进制数表示的权限模式,用于设置保存文件的权限。默认为 0o644。
directory_permissions_mode: 八进制数表示的权限模式,用于设置保存文件夹的权限。默认为 0o755# 方法
init(self, location=None, base_url=None, file_permissions_mode=None, directory_permissions_mode=None): 初始化方法,可设置存储的位置、基础URL以及文件和目录的权限模式。

open(self, name, mode='rb'): 打开一个文件并返回一个文件对象。模式默认为只读 ('rb')。

save(self, name, content, max_length=None): 保存内容到文件系统中。name 是文件名,content 参数必须是 django.core.files.File 的对象或子类对象,或者是一个可以用 File 包装的类文件对象。,可选参数 max_length 用来限制文件大小。

path(self, name): 返回给定文件名的完整文件系统路径。

url(self, name): 给定文件名,返回该文件的完整URL。

delete(self, name): 删除指定文件。

exists(self, name): 检查指定文件是否存在。

listdir(self, path=''): 返回指定目录下的文件和目录列表。默认列出根目录的内容。

size(self, name): 返回指定文件的大小(以字节为单位)。

modified_time(self, name): 返回文件最后修改的时间。

accessed_time(self, name): 返回文件最后访问的时间。

created_time(self, name): 返回文件创建的时间。

get_valid_name(self, name): 返回一个合法的文件名,确保文件名适合在文件系统中使用。

get_available_name(self, name): 返回一个可用的文件名,用于解决文件名冲突问题。
FileSystemStorage类的源码
点击查看代码
@deconstructible
class FileSystemStorage(Storage):
    """
    Standard filesystem storage
    """
    # The combination of O_CREAT and O_EXCL makes os.open() raise OSError if
    # the file already exists before it's opened.
    OS_OPEN_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0)

    def __init__(self, location=None, base_url=None, file_permissions_mode=None,
                 directory_permissions_mode=None):
        self._location = location
        self._base_url = base_url
        self._file_permissions_mode = file_permissions_mode
        self._directory_permissions_mode = directory_permissions_mode
        setting_changed.connect(self._clear_cached_properties)

    def _clear_cached_properties(self, setting, **kwargs):
        """Reset setting based property values."""
        if setting == 'MEDIA_ROOT':
            self.__dict__.pop('base_location', None)
            self.__dict__.pop('location', None)
        elif setting == 'MEDIA_URL':
            self.__dict__.pop('base_url', None)
        elif setting == 'FILE_UPLOAD_PERMISSIONS':
            self.__dict__.pop('file_permissions_mode', None)
        elif setting == 'FILE_UPLOAD_DIRECTORY_PERMISSIONS':
            self.__dict__.pop('directory_permissions_mode', None)

    def _value_or_setting(self, value, setting):
        return setting if value is None else value

    @cached_property
    def base_location(self):
        return self._value_or_setting(self._location, settings.MEDIA_ROOT)

    @cached_property
    def location(self):
        return os.path.abspath(self.base_location)

    @cached_property
    def base_url(self):
        if self._base_url is not None and not self._base_url.endswith('/'):
            self._base_url += '/'
        return self._value_or_setting(self._base_url, settings.MEDIA_URL)

    @cached_property
    def file_permissions_mode(self):
        return self._value_or_setting(self._file_permissions_mode, settings.FILE_UPLOAD_PERMISSIONS)

    @cached_property
    def directory_permissions_mode(self):
        return self._value_or_setting(self._directory_permissions_mode, settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS)

    def _open(self, name, mode='rb'):
        return File(open(self.path(name), mode))

    def _save(self, name, content):
        full_path = self.path(name)

        # Create any intermediate directories that do not exist.
        directory = os.path.dirname(full_path)
        try:
            if self.directory_permissions_mode is not None:
                # Set the umask because os.makedirs() doesn't apply the "mode"
                # argument to intermediate-level directories.
                old_umask = os.umask(0o777 & ~self.directory_permissions_mode)
                try:
                    os.makedirs(directory, self.directory_permissions_mode, exist_ok=True)
                finally:
                    os.umask(old_umask)
            else:
                os.makedirs(directory, exist_ok=True)
        except FileExistsError:
            raise FileExistsError('%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:
                    # The current umask value is masked out by os.open!
                    fd = os.open(full_path, self.OS_OPEN_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 FileExistsError:
                # A new name is needed if the file exists.
                name = self.get_available_name(name)
                full_path = self.path(name)
            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)

        # Ensure the saved path is always relative to the storage root.
        name = os.path.relpath(full_path, self.location)
        # Store filenames with forward slashes, even on Windows.
        return str(name).replace('\\', '/')

    def delete(self, name):
        assert name, "The name argument is not allowed to be empty."
        name = self.path(name)
        # If the file or directory exists, delete it from the filesystem.
        try:
            if os.path.isdir(name):
                os.rmdir(name)
            else:
                os.remove(name)
        except FileNotFoundError:
            # FileNotFoundError is raised if the file or directory was removed
            # concurrently.
            pass

    def exists(self, name):
        return os.path.exists(self.path(name))

    def listdir(self, path):
        path = self.path(path)
        directories, files = [], []
        for entry in os.scandir(path):
            if entry.is_dir():
                directories.append(entry.name)
            else:
                files.append(entry.name)
        return directories, files

    def path(self, name):
        return safe_join(self.location, name)

    def size(self, name):
        return os.path.getsize(self.path(name))

    def url(self, name):
        if self.base_url is None:
            raise ValueError("This file is not accessible via a URL.")
        url = filepath_to_uri(name)
        if url is not None:
            url = url.lstrip('/')
        return urljoin(self.base_url, url)

    def _datetime_from_timestamp(self, ts):
        """
        If timezone support is enabled, make an aware datetime object in UTC;
        otherwise make a naive one in the local timezone.
        """
        if settings.USE_TZ:
            # Safe to use .replace() because UTC doesn't have DST
            return datetime.utcfromtimestamp(ts).replace(tzinfo=timezone.utc)
        else:
            return datetime.fromtimestamp(ts)

    def get_accessed_time(self, name):
        return self._datetime_from_timestamp(os.path.getatime(self.path(name)))

    def get_created_time(self, name):
        return self._datetime_from_timestamp(os.path.getctime(self.path(name)))

    def get_modified_time(self, name):
        return self._datetime_from_timestamp(os.path.getmtime(self.path(name)))


def get_storage_class(import_path=None):
    return import_string(import_path or settings.DEFAULT_FILE_STORAGE)


class DefaultStorage(LazyObject):
    def _setup(self):
        self._wrapped = get_storage_class()()


default_storage = DefaultStorage()


二、django 文件上传步步骤

1、在settings.py中配置上传路径

点击查看代码
# 指定django默认的文件存储系统,如果不指定,默认就是FileSystemStorage,如果想要使用自定义的文件存储系统,则必需指定
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'


# 访问上传文件的url
MEDIA_URL = '/media/'

# 上传文件的存储路径
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')


2、编写文件上传工具类

点击查看代码
from django.core.files.storage import default_storage
from library_management.settings import MEDIA_ROOT


# 文件上传
class UploadFile:
    @staticmethod
    def upload(file, destination=MEDIA_ROOT, filename=None):
        """
        :param file: request.FILES.get('参数名')获取到的文件对象
        :param destination: 文件上传后的存储目录
        :param filename: 文件上传后的名字
        :return:
        """
        if not file:
            raise ValueError("文件不能为空")
        if file.size > 1024 * 1024 * 1024:
            raise ValueError("文件大小不能超过1G")
        try:
            if filename is None:
                filename = file.name
            # 可以从源码中看到,default_storage是django提供的FileSystemStorage对象
            file_path = default_storage.save(os.path.join(destination, filename), file)
            return file_path
        except Exception as e:
            print(e)
            return False


3、在视图中调用

点击查看代码
class UploadAvatar(APIView):
    def post(self, request):
        try:
            file = request.FILES.get('file1')
            # 调用工具类保存文件
            file_path = UploadFile.upload(file=file)
            if file_path:
                return Response({"file": file_path, "message": "文件上传成功"}, status=status.HTTP_200_OK)
            else:
                return Response({"message": "文件上传失败"}, status=status.HTTP_422_UNPROCESSABLE_ENTITY)
        except Exception as e:
            return Response({"message": "文件上传失败"}, status=status.HTTP_422_UNPROCESSABLE_ENTITY)


posted @   有形无形  阅读(33)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示