
在 django 中,`filefield` 的文件内容在 `save()` 方法执行前尚未写入磁盘,直接通过本地路径(如 `"media/upload/..."`)访问会触发 `filenotfounderror`。正确做法是通过 `self.file.read()` 获取原始字节流,在内存中完成图像处理后再写回或生成衍生数据。
Django 的 FileField 并非立即将上传文件写入磁盘——它仅在调用 save() 时才真正执行文件存储(由底层 Storage 后端完成)。因此,在 save() 方法开头尝试用 Image.open("media/upload/...") 打开文件必然失败,因为此时文件尚未落盘。
✅ 正确思路是:在 save() 中通过 self.file.read() 获取原始字节流 → 在内存中处理(如 PIL 图像操作)→ 提取元信息、生成缩略图、计算 thumbhash 等 → 再调用 super().save() 完成持久化。
以下是修正后的 Media 模型示例(关键改动已加注释):
import os
from io import BytesIO
from PIL import Image
from django.core.files.base import ContentFile
from django.db import models
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. 确保 filename 已设置(如未传,可从 file.name 推导)
if not self.filename:
self.filename = os.path.basename(self.file.name) or "uploaded_file"
# ✅ 2. 读取原始文件字节流(注意:read() 后指针偏移,需重置)
self.file.seek(0) # 重置文件指针到开头
file_bytes = self.file.read()
# ✅ 3. 使用 BytesIO 在内存中打开图像(避免依赖磁盘路径)
try:
image = Image.open(BytesIO(file_bytes))
image_format = image.format or 'JPEG'
mime_type = Image.MIME.get(image_format, 'image/jpeg')
except Exception as e:
raise ValueError(f"Invalid image file: {e}")
# ✅ 4. 处理缩略图(同样在内存中操作,最后写入 media/cache/)
sizes = [(150, 150), (256, 256)]
thumbnail = {}
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"
ext = image_format.lower()
if ext == "jpg":
ext = "jpeg"
file_path = os.path.join(cache_dir, f"{self.id}-resized-{self.filename}-{index}.{ext}")
# 将处理后的图像保存到指定路径
resized.save(file_path, format=image_format)
thumbnail[f"{w}x{h}"] = file_path # 建议用语义化 key,如 "150x150"
# ✅ 5. 设置字段值(注意:size 是原始文件大小,非处理后)
self.mime_type = mime_type
self.size = len(file_bytes) # ✅ 使用字节长度,更准确
self.thumbnail = thumbnail
self.url = f"http://127.0.0.1:8000/media/upload/{self.filename}"
self.thumbhash = image_to_thumbhash(image) # 假设该函数接受 PIL.Image
# ✅ 6. 调用父类 save —— 此时 file 字段才会真正写入磁盘
super().save(*args, **kwargs)⚠️ 重要注意事项:
- self.file.read() 后必须调用 self.file.seek(0)(若后续还需读取),否则 super().save() 可能写入空文件;
- 若需修改原始上传文件内容(如压缩后覆盖),应使用 ContentFile 重新赋值:
self.file.save(self.file.name, ContentFile(processed_bytes), save=False) - upload_to 路径是相对 MEDIA_ROOT 的,确保 settings.MEDIA_ROOT = "media" 已正确定义;
- 生产环境请使用 django.core.files.storage.default_storage 进行路径操作,避免硬编码 "media/";
- 缩略图路径建议存为相对路径(如 "cache/xxx.jpg"),配合 MEDIA_URL 构建前端可访问 URL,而非绝对磁盘路径。
通过这种基于内存流的处理方式,既规避了文件未就绪的竞态问题,又保持了逻辑内聚性,是 Django 中处理上传文件预处理的标准范式。










