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 ContentFileContentFile 类继承自 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 对象(或一个子类对象)。该对象中包含一些常用的方法和属性:
点击查看代码
方法:
1、read()
# 从文件中读取整个上传的数据。小心使用这个方法:如果上传的文件很大,如果你试图把它读到内存中,它可能会让你的系统不堪重负。
2、multiple_chunks(chunk_size=None)
如果上传的文件足够大,需要分块读取,返回 True。默认情况下是大于 2.5MB的文件就需要分块。
3、chunks(chunk_size=None)
返回一个生成器,用于逐块读取大文件内容,避免内存溢出。如果 multiple_chunks() 是 True,你应该在循环中使用这个方法而不是 read()。
在实践中,通常最简单的做法是使用 chunks() 而不是使用 read() ,这可以确保大文件不会过度占用系统的内存。
属性:
1、name
上传的文件名称(如 my_file.txt)
2、size
上传文件的大小,以字节为单位。
3、content_type_extra
包含传递给 content-type 头的额外参数的字典。
4、charset
对于 text/* 内容类型,浏览器提供的字符集(即 utf8)。
InMemoryUploadedFile类
在django中的位置:
from django.core.files.uploadedfile import UploadedFile,InMemoryUploadedFileInMemoryUploadedFile 类用于处理那些大小适中、可以直接加载到内存中的上传文件。它是 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.FileSystemStorageFileSystemStorage 类是用于处理文件存储的类,它实现了将文件保存到本地文件系统的存储策略。这是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)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· 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