【问题标题】:Django delete FileFieldDjango 删除 FileField
【发布时间】:2013-04-09 02:31:31
【问题描述】:

我正在用 Django 构建一个网络应用程序。我有一个上传文件的模型,但我无法删除该文件。这是我的代码:

class Song(models.Model):
    name = models.CharField(blank=True, max_length=100)
    author = models.ForeignKey(User, to_field='id', related_name="id_user2")
    song = models.FileField(upload_to='/songs/')
    image = models.ImageField(upload_to='/pictures/', blank=True)
    date_upload = models.DateField(auto_now_add=True)

    def delete(self, *args, **kwargs):
        # You have to prepare what you need before delete the model
        storage, path = self.song.storage, self.song.path
        # Delete the model before the file
        super(Song, self).delete(*args, **kwargs)
        # Delete the file after the model
        storage.delete(path)

然后,在python manage.py shell 我这样做:

song = Song.objects.get(pk=1)
song.delete()

它从数据库中删除记录,但不删除服务器上的文件。 我还能尝试什么?

谢谢!

【问题讨论】:

标签: django django-models


【解决方案1】:

在 Django 1.3 之前,当您删除相应的模型实例时,该文件会自动从文件系统中删除。您可能使用的是较新的 Django 版本,因此您必须自己实现从文件系统中删除文件。

简单的基于信号的样本

在撰写本文时,我选择的方法是混合使用 post_deletepre_save 信号,这样就可以在删除相应模型或更改文件时删除过时的文件。

基于假设的MediaFile 模型:

import os
import uuid

from django.db import models
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _


class MediaFile(models.Model):
    file = models.FileField(_("file"),
        upload_to=lambda instance, filename: str(uuid.uuid4()))


# These two auto-delete files from filesystem when they are unneeded:

@receiver(models.signals.post_delete, sender=MediaFile)
def auto_delete_file_on_delete(sender, instance, **kwargs):
    """
    Deletes file from filesystem
    when corresponding `MediaFile` object is deleted.
    """
    if instance.file:
        if os.path.isfile(instance.file.path):
            os.remove(instance.file.path)

@receiver(models.signals.pre_save, sender=MediaFile)
def auto_delete_file_on_change(sender, instance, **kwargs):
    """
    Deletes old file from filesystem
    when corresponding `MediaFile` object is updated
    with new file.
    """
    if not instance.pk:
        return False

    try:
        old_file = MediaFile.objects.get(pk=instance.pk).file
    except MediaFile.DoesNotExist:
        return False

    new_file = instance.file
    if not old_file == new_file:
        if os.path.isfile(old_file.path):
            os.remove(old_file.path)
  • 我认为我之前构建的其中一个应用程序在生产中使用了此代码,但使用风险自负。
  • 例如,有一个可能的数据丢失场景:如果您的save() 方法调用恰好在回滚的事务中,您的数据最终可能会引用一个不存在的文件。您可以考虑将文件删除逻辑包装到transaction.on_commit() 中,类似于transaction.on_commit(lambda: os.remove(old_file.path))as suggested in Mikhail’s commentdjango-cleanup图书馆does something along those lines
  • 极端情况:如果您的应用上传新文件并将模型实例指向新文件而不调用save()(例如通过批量更新QuerySet),则旧文件将继续存在,因为信号不会跑步。如果您使用传统的文件处理方法,则不会发生这种情况。
  • 编码风格:本例使用file作为字段名,这不是一个好的风格,因为它与内置的file对象标识符冲突。

附录:定期清理

实际上,您可能希望运行一个定期任务来处理孤立文件清理,以防运行时故障阻止某些文件被删除。考虑到这一点,您可能可以完全摆脱信号处理程序,并将这样的任务机制用于处理不敏感的数据和不太大的文件。

无论哪种方式,如果您处理敏感数据,最好反复或三次检查您是否及时删除生产中的数据以避免任何相关责任。

另见

  • FieldFile.delete() 在 Django 1.11 模型字段参考中(请注意,它描述了 FieldFile 类,但您可以直接在字段上调用 ​​.delete()FileField 实例代理到相应的 FieldFile 实例,以及您可以像访问字段一样访问它的方法)

    请注意,删除模型时,不会删除相关文件。如果您需要清理孤立文件,则需要自己处理(例如,使用可以手动运行或计划通过例如 cron 定期运行的自定义管理命令)。

  • 为什么 Django 不自动删除文件:entry in release notes for Django 1.3

    在早期的 Django 版本中,当删除包含 FileField 的模型实例时,FileField 会自行从后端存储中删除文件。这为几种数据丢失场景打开了大门,包括回滚事务和引用同一文件的不同模型上的字段。在 Django 1.3 中,当模型被删除时,FileFielddelete() 方法将不会被调用。如果您需要清理孤立文件,则需要自己处理(例如,使用可以手动运行或计划通过例如 cron 定期运行的自定义管理命令)。

  • Example of using a pre_delete signal only

【讨论】:

  • 是的,但请确保进行适当的检查。 (等一下,我把我在实际系统中使用的代码贴出来。)
  • 最好使用instance.song.delete(save=False),因为它使用了正确的django存储引擎。
  • 现在很少有我复制我无法直接从 SO 编写自己的代码,并且它可以在有限的修改下工作。很棒的帮助,谢谢!
  • 在此发现一个错误,如果实例存在,但之前没有保存图像,则os.path.isfile(old_file.path) 失败,因为old_file.path 引发错误(没有文件与该字段关联)。我通过在调用os.path.isfile() 之前添加if old_file: 来修复它。
  • 最好调用transaction.on_commit(lambda: os.remove(old_file.path)),因为如果你删除了一个文件,然后发生事务回滚,你就丢失了一个文件
【解决方案2】:

试试django-cleanup,当你删除模型时,它会自动调用 FileField 上的 delete 方法。

pip install django-cleanup

settings.py

INSTALLED_APPS = (
     ...
    'django_cleanup.apps.CleanupConfig',
)

【讨论】:

  • 酷,需要默认添加到FileField,谢谢!
  • 上传的同时删除文件
  • 哇。我试图让这不会发生,但我不知道为什么会这样。有人在几年前安装了这个并忘记了它。谢谢。
  • 那么,为什么 Django 首先删除了文件字段删除功能?
  • @ha-neul 正如Django doc所说,这是为了防止数据丢失并支持回滚。
【解决方案3】:

您可以使用 Django >= 1.10 调用文件字段的.delete 方法从文件系统中删除文件,如下所示:

obj = Song.objects.get(pk=1)
obj.song.delete()

【讨论】:

  • 应该是公认的答案,简单而有效。
【解决方案4】:

您也可以简单地覆盖模型的删除函数以检查文件是否存在并在调用超级函数之前将其删除。

import os

class Excel(models.Model):
    upload_file = models.FileField(upload_to='/excels/', blank =True)   
    uploaded_on = models.DateTimeField(editable=False)


    def delete(self,*args,**kwargs):
        if os.path.isfile(self.upload_file.path):
            os.remove(self.upload_file.path)

        super(Excel, self).delete(*args,**kwargs)

【讨论】:

  • 请注意,调用queryset.delete() 不会使用此解决方案清理文件。您需要遍历查询集并在每个对象上调用 .delete()
  • 我是 Django 新手。这很好,但是如果模型是从一个重写了 delete 方法的抽象类继承的,这不会重写抽象类的那个吗?使用信号对我来说似乎更好
【解决方案5】:

Django 2.x 解决方案:

Django 2 中处理文件删除非常容易。我尝试过使用 Django 2 和 SFTP Storage 以及 FTP STORAGE 的以下解决方案,我很确定它可以与任何其他实现 delete 方法的存储管理器一起使用。 (delete 方法是 storage 抽象方法之一,它应该从存储中物理删除文件!)

覆盖模型的 delete 方法,使实例在删除自身之前删除其 FileFields:

class Song(models.Model):
    name = models.CharField(blank=True, max_length=100)
    author = models.ForeignKey(User, to_field='id', related_name="id_user2")
    song = models.FileField(upload_to='/songs/')
    image = models.ImageField(upload_to='/pictures/', blank=True)
    date_upload = models.DateField(auto_now_add=True)

    def delete(self, using=None, keep_parents=False):
        self.song.storage.delete(self.song.name)
        self.image.storage.delete(self.image.name)
        super().delete()

它对我来说很容易。 如果要在删除前检查文件是否存在,可以使用storage.exists。例如self.song.storage.exists(self.song.name) 将返回一个 boolean 表示歌曲是否存在。所以它看起来像这样:

def delete(self, using=None, keep_parents=False):
    # assuming that you use same storage for all files in this model:
    storage = self.song.storage

    if storage.exists(self.song.name):
        storage.delete(self.song.name)

    if storage.exists(self.image.name):
        storage.delete(self.image.name)

    super().delete()

编辑(另外):

正如@HeyMan 提到的,使用此解决方案调用Song.objects.all().delete() 不会删除文件!这是因为Song.objects.all().delete() 正在运行Default Manager 的删除查询。因此,如果您希望能够使用objects 方法删除模型的文件,则必须编写并使用Custom Manager(仅用于覆盖其删除查询):

class CustomManager(models.Manager):
    def delete(self):
        for obj in self.get_queryset():
            obj.delete()

为了将CustomManager 分配给模型,您必须在模型中初始化objects

class Song(models.Model):
    name = models.CharField(blank=True, max_length=100)
    author = models.ForeignKey(User, to_field='id', related_name="id_user2")
    song = models.FileField(upload_to='/songs/')
    image = models.ImageField(upload_to='/pictures/', blank=True)
    date_upload = models.DateField(auto_now_add=True)
    
    objects = CustomManager() # just add this line of code inside of your model

    def delete(self, using=None, keep_parents=False):
        self.song.storage.delete(self.song.name)
        self.image.storage.delete(self.image.name)
        super().delete()

现在您可以在任何objects 子查询的末尾使用.delete()。我写了最简单的CustomManager,但你可以通过返回一些关于你删除的对象或任何你想要的东西来做得更好。

【讨论】:

  • 是的,我认为他们在我发布问题后添加了该功能。
  • 仍然删除不调用 wenn 调用 Song.objects.all().delete()。当实例被 on_delete=models.CASCADE 删除时也是如此。
  • @HeyMan 我解决了它并立即编辑了我的解决方案:)
  • 我喜欢你的解决方案!不幸的是,当我在QuerySet 上调用delete() 时,这个仍然没有调用删除。根据文档,即使在 Django 3 中,也必须实现 post_delete 信号才能完全实现。
  • @BarneySzabolcs tnx,您是否尝试过使用CustomManager 我在此答案的第二部分中提出的建议?我会检查 Django 3 并更新答案,tnx 提到:)
【解决方案6】:

这是一个可以在删除模型或上传新文件时删除旧文件的应用:django-smartfields

from django.db import models
from smartfields import fields

class Song(models.Model):
    song = fields.FileField(upload_to='/songs/')
    image = fields.ImageField(upload_to='/pictures/', blank=True)

【讨论】:

  • 我认为这应该是公认的答案。这是最简洁的解决方案,也可能是最好的实现,因为它是一个库,因此人们可以随时做出贡献。
  • 不,不幸的是 django-smartfields 可能有一个错误,因为它不会删除我的文件,至少在使用 QuerySets 时不会。也许django-cleanup
【解决方案7】:

@Anton Strogonoff

当文件更改时,我在代码中遗漏了一些东西,如果创建新文件会产生错误,因为是新文件没有找到路径。我修改了函数的代码并添加了一个 try/except 语句,它运行良好。

@receiver(models.signals.pre_save, sender=MediaFile)
def auto_delete_file_on_change(sender, instance, **kwargs):
    """Deletes file from filesystem
    when corresponding `MediaFile` object is changed.
    """
    if not instance.pk:
        return False

    try:
        old_file = MediaFile.objects.get(pk=instance.pk).file
    except MediaFile.DoesNotExist:
        return False

    new_file = instance.file
    if not old_file == new_file:
        try:
            if os.path.isfile(old_file.path):
                os.remove(old_file.path)
        except Exception:
            return False

【讨论】:

  • 我没有遇到过这种情况——可能是我的代码中的错误,或者 Django 中的某些更改。不过,我建议您在 try: 块中捕获特定异常(也许是AttributeError?)。
  • 使用 os 库不是一个好主意,因为如果您迁移到不同的存储(例如 Amazon S3)会遇到问题。
  • @IgorPomaranskiy 当您使用 os.remove 时,在 Amazon S3 这样的存储中会发生什么??
  • @DanielGonzálezFernández 我猜它会失败(出现类似不存在路径的错误)。这就是 Django 使用抽象来存储的原因。
【解决方案8】:

对于那些在较新版本的 Django(当前为 3.1)中寻找答案的人。

我找到了这个website,它对我有用,没有任何更改,只需将它添加到您的models.py

from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.db import models
 
""" Only delete the file if no other instances of that model are using it"""    
def delete_file_if_unused(model,instance,field,instance_file_field):
    dynamic_field = {}
    dynamic_field[field.name] = instance_file_field.name
    other_refs_exist = model.objects.filter(**dynamic_field).exclude(pk=instance.pk).exists()
    if not other_refs_exist:
        instance_file_field.delete(False)
""" Whenever ANY model is deleted, if it has a file field on it, delete the associated file too"""
@receiver(post_delete)
def delete_files_when_row_deleted_from_db(sender, instance, **kwargs):
    for field in sender._meta.concrete_fields:
        if isinstance(field,models.FileField):
            instance_file_field = getattr(instance,field.name)
            delete_file_if_unused(sender,instance,field,instance_file_field)
            
""" Delete the file if something else get uploaded in its place"""
@receiver(pre_save)
def delete_files_when_file_changed(sender,instance, **kwargs):
    # Don't run on initial save
    if not instance.pk:
        return
    for field in sender._meta.concrete_fields:
        if isinstance(field,models.FileField):
            #its got a file field. Let's see if it changed
            try:
                instance_in_db = sender.objects.get(pk=instance.pk)
            except sender.DoesNotExist:
                # We are probably in a transaction and the PK is just temporary
                # Don't worry about deleting attachments if they aren't actually saved yet.
                return
            instance_in_db_file_field = getattr(instance_in_db,field.name)
            instance_file_field = getattr(instance,field.name)
            if instance_in_db_file_field.name != instance_file_field.name:
                delete_file_if_unused(sender,instance,field,instance_in_db_file_field)

【讨论】:

    【解决方案9】:

    每次我上传新图像(徽标字段)时都会运行此代码,并检查徽标是否已经存在,如果存在,请将其关闭并将其从磁盘中删除。当然可以在接收器功能中进行相同的过程。希望这会有所帮助。

     #  Returns the file path with a folder named by the company under /media/uploads
        def logo_file_path(instance, filename):
            company_instance = Company.objects.get(pk=instance.pk)
            if company_instance.logo:
                logo = company_instance.logo
                if logo.file:
                    if os.path.isfile(logo.path):
                        logo.file.close()
                        os.remove(logo.path)
    
            return 'uploads/{0}/{1}'.format(instance.name.lower(), filename)
    
    
        class Company(models.Model):
            name = models.CharField(_("Company"), null=False, blank=False, unique=True, max_length=100) 
            logo = models.ImageField(upload_to=logo_file_path, default='')
    

    【讨论】:

      猜你喜欢
      • 2018-12-14
      • 1970-01-01
      • 2018-07-05
      • 1970-01-01
      • 1970-01-01
      • 2014-12-19
      • 1970-01-01
      • 2011-08-22
      • 2011-05-01
      相关资源
      最近更新 更多