Django 模型中如何在 save() 方法内安全处理上传文件

Django 模型中如何在 save() 方法内安全处理上传文件

在 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)

⚠️ 重要注意事项:

站长俱乐部购物系统

站长俱乐部购物系统

功能介绍:1、模块化的程序设计,使得前台页面设计与程序设计几乎完全分离。在前台页面采用过程调用方法。在修改页面设计时只需要在相应位置调用设计好的过程就可以了。另外,这些过程还提供了不同的调用参数,以实现不同的效果;2、阅读等级功能,可以加密产品,进行收费管理;3、可以完全可视化编辑文章内容,所见即所得;4、无组件上传文件,服务器无需安装任何上传组件,无需支持FSO,即可上传文件。可限制文件上传的类

下载

  • 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, …) 安全构造路径,永远不要假设未保存文件已存在于磁盘。

https://www.php.cn/faq/2005203.html

发表回复

Your email address will not be published. Required fields are marked *