
在 django 中,直接通过文件路径访问 `filefield` 未保存的文件会导致 filenotfounderror;正确做法是读取文件字节流,在内存中完成图像处理后再写回字段,避免依赖尚未创建的物理路径。
Django 的 FileField 在模型实例调用 save() 前不会将文件写入磁盘——它仅在 super().save() 执行时才触发文件上传与存储。因此,像 media_path = "media/upload/..." 这样的硬编码路径在 save() 早期阶段必然不存在,Image.open(media_path) 必然失败。
✅ 正确方案:全程操作文件字节流(in-memory),不依赖磁盘路径。以下是优化后的 Media.save() 实现:
from PIL import Image
from io import BytesIO
import os
class Media(models.Model):
title = models.CharField(max_length=255, null=True, blank=True)
file = models.FileField(upload_to="upload/")
filename = models.CharField(max_length=255, null=True, blank=True)
mime_type = models.CharField(max_length=255, null=True, blank=True)
thumbnail = models.JSONField(null=True, blank=True)
size = models.FloatField(null=True, blank=True)
url = models.CharField(max_length=300, null=True, blank=True)
thumbhash = models.CharField(max_length=255, blank=True, null=True)
is_public = models.BooleanField(blank=True, null=True)
def save(self, *args, **kwargs):
# ✅ 1. 重置文件指针并读取原始字节(关键!)
self.file.seek(0) # 确保从头读取
original_bytes = self.file.read()
# ✅ 2. 使用 BytesIO 构建内存图像对象
image = Image.open(BytesIO(original_bytes))
mime_type = image.get_format_mimetype()
format_ext = mime_type.split("/")[-1].lower()
# ✅ 3. 处理缩略图(同样在内存中完成)
sizes = [(150, 150), (256, 256)]
thumbnail = {}
# 创建 cache 目录(确保存在)
cache_dir = os.path.join("media", "cache")
os.makedirs(cache_dir, exist_ok=True)
for i, (w, h) in enumerate(sizes):
resized = image.resize((w, h), Image.Resampling.LANCZOS)
index = "small" if i == 0 else "medium"
# 生成唯一缓存路径(注意:使用 self.pk 仅在更新时可靠;新建时用临时命名或延迟生成)
cache_name = f"{self.pk or 'tmp'}-resized-{self.filename or 'unknown'}-{index}.{format_ext}"
cache_path = os.path.join(cache_dir, cache_name)
# 保存到磁盘(此时 media/cache 已存在)
resized.save(cache_path, format=format_ext.upper())
thumbnail[f"{w}x{h}"] = f"cache/{cache_name}" # 存储相对路径,便于前端访问
# ✅ 4. 更新字段(注意:filename 应由 upload_to 或逻辑自动推导,不建议手动设)
if not self.filename:
self.filename = os.path.basename(self.file.name)
self.mime_type = mime_type
self.size = len(original_bytes)
self.thumbnail = thumbnail
self.url = self.file.url # ✅ 使用 Django 自动提供的 URL(更健壮)
self.thumbhash = image_to_thumbhash(image) # 假设该函数接受 PIL.Image
# ✅ 5. 写回处理后的字节(可选:若需修改原文件内容)
# self.file = ContentFile(processed_bytes, name=self.file.name)
super().save(*args, **kwargs)⚠️ 重要注意事项:
- self.file.seek(0) 不可省略:Django 文件对象在序列化/上传后指针可能位于末尾,不重置将读取空内容;
- 不要硬编码 media/ 路径:Django 静态/媒体文件路径应通过 settings.MEDIA_ROOT 获取,或使用 default_storage;
- self.pk 在首次保存时为 None:生成缓存文件名时需兼容(如用 uuid.uuid4() 替代);
- 避免重复保存大文件:若仅需生成缩略图而无需修改原图,无需 self.file.write(...) —— 上述示例中 write 并非必须,除非你确实要覆盖原始文件内容;
- Serializer 中 create 方法有误:当前 return Media(**validated_data) 不会调用 save(),应改为 Media.objects.create(...) 或显式调用 save()。
? 总结:Django 文件处理的核心原则是——信任 FileField 的抽象接口,用 .read()/.seek() 操作字节流,用 default_storage 或 os.path.join(settings.MEDIA_ROOT, ...) 安全构造路径,永远不要假设未保存文件已存在于磁盘。









