Django删除FileField

144

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

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

它会从数据库中删除记录,但不会删除服务器上的文件。 还有什么其他的尝试方法吗?

谢谢!


直接使用default_storage怎么样?https://docs.djangoproject.com/en/dev/topics/files/ - MGP
11个回答

211

在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))正如Mikhail的评论所建议的那样django-cleanup也是这样做的
  • 边缘情况:如果您的应用程序上传新文件并将模型实例指向新文件而不调用save()(例如通过批量更新QuerySet),则旧文件将保留下来,因为信号不会运行。如果您使用传统的文件处理方法,则不会发生这种情况。
  • 编码风格:此示例使用file作为字段名称,这不是一个好的风格,因为它与内置的file对象标识符冲突。

补充:定期清理

从实际出发,你可能需要运行一个定期任务来处理孤立的文件清理,以防运行时故障阻止某些文件被删除。因此,你可以完全摆脱信号处理程序,并将这样的任务作为处理不敏感数据和不是太大的文件的机制。

无论如何,如果你正在处理敏感数据,则最好进行双重或三重检查,以确保你在生产中及时删除数据,以避免任何相关责任。

另见

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

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

  • 为什么Django不会自动删除文件:Django 1.3版本发布说明中的条目

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

  • 仅使用pre_delete信号的示例


9
最好使用instance.song.delete(save=False),因为它使用了正确的Django存储引擎。 - Eduardo
发现了一个错误,如果实例存在但之前没有保存图像,则os.path.isfile(old_file.path)会失败,因为old_file.path会引发错误(该字段未关联任何文件)。我通过在调用os.path.isfile()之前添加if old_file:来修复它。 - three_pineapples
@three_pineapples 说得有道理。可能是文件字段上的 NOT NULL 约束被绕过或在某些时候不存在,因此一些对象会为空。 - Anton Strogonoff
3
最好调用 transaction.on_commit(lambda: os.remove(old_file.path)),因为如果你删除了一个文件,然后事务回滚发生,你就会丢失一个文件。 - Mikhail
1
@Mikhail 说得好,编辑后添加。 django-cleanup 看起来做了类似的事情。 - Anton Strogonoff
显示剩余2条评论

117

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

pip install django-cleanup

设置.py

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

1
在上传文件的同时,它正在删除该文件。 - chirag soni
1
哇,我一直在努力避免这种情况发生,但我一直搞不清楚原因。原来是有人几年前安装了这个并忘记了它的存在。谢谢。 - ryan28561
7
那么,为什么Django一开始要删除filefield的删除功能? - ha-neul
3
Django文档所述,这是为了防止数据丢失和支持回滚。 - mathias.lantean
@un1t 有没有办法排除某个特定的文件或文件不被删除?例如,我不想让我的 default.png 被删除。我该如何排除它? - SohailAQ
显示剩余2条评论

60

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

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

12
应该被接受为答案,简单而且有效。 - Nikolay Shindarov
1
我应该把这段代码放在哪里?放在 views.py 还是 models.py 中? - Shayan
@Shayan 在 views.py 中。 - dibery

21

Django 2.x 解决方案:

在 Django 2 中处理文件删除非常简单。我已经试过使用 Django 2 和 SFTP 存储以及 FTP 存储的以下解决方案,并且我非常确信它将与实现了 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()正在运行默认管理器的删除查询。因此,如果想要能够通过使用objects方法删除模型中的文件,则必须编写和使用自定义管理器(仅用于覆盖其删除查询):

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,但你可以通过返回有关已删除对象或其它你想要的信息来更好地完成。


1
是的,我认为自从我发布问题以来,他们已经添加了该功能。 - Marcos Aguayo
1
仍然无法调用Song.objects.all().delete()时删除。当实例被on_delete=models.CASCADE删除时也是如此。 - HeyMan
@HeyMan 我解决了它并立即编辑了我的解决方案 :) - Hamidreza
我很喜欢你的解决方案!不幸的是,即使在Django 3中,当我在QuerySet上调用delete()时,它仍然没有调用delete。根据文档,在完全实现时,还必须实现post_delete信号。 - Barney Szabolcs
@Hamidreza 是的,我非常惊讶它没有起作用。在实施了这篇文章中的答案后,我发现 django_cleanup 也可以与 QuerySet 一起使用,而无需任何设置。 - Barney Szabolcs
显示剩余2条评论

20

您还可以简单地重写模型的删除函数,以检查文件是否存在并在调用超级函数之前删除它。

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)

13
请注意,调用queryset.delete()不会使用此解决方案清理文件。您需要遍历查询集并在每个对象上调用.delete() - Scott Woodall
我对Django很新。这很好,但是如果模型继承自覆盖了delete方法的抽象类,那么不会覆盖抽象类中的方法吗?在我看来,使用信号似乎更好。 - theTypan

5

这里有一个应用程序,当模型被删除或新文件被上传时,它会删除旧文件: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)

我认为这应该是被接受的答案。这是最干净的解决方案,也可能是最好的实现,因为它是一个库,所以人们可以随时贡献。 - Barney Szabolcs
不幸的是,django-smartfields可能存在一个错误,因为当使用QuerySet时,它无法删除我的文件,至少目前是这样。也许可以尝试使用django-cleanup? - Barney Szabolcs

5

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

我发现了这个网站,它对我非常有效,无需任何更改,只需在您的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)

3

@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

我还没有遇到这个问题 - 可能是我的代码中有一个 bug,或者 Django 发生了某些变化。不过,我建议在你的 try: 块中捕获特定的异常(例如:AttributeError?)。 - Anton Strogonoff
2
使用os库不是一个很好的想法,因为如果您迁移到不同的存储(例如Amazon S3),则可能会遇到问题。 - Ihor Pomaranskyy
@IgorPomaranskiy 如果你在 Amazon S3 这样的存储中使用 os.remove,会发生什么情况? - Daniel González Fernández
@DanielGonzálezFernández 我猜它会失败(出现关于不存在路径的错误之类的)。这就是为什么Django使用存储的抽象。 - Ihor Pomaranskyy

1
我相信这只是简单地从对象中删除文件/图像,然后再删除对象本身。

template.html

<a href="{% url 'delete_song' song.id %}" value="Delete">Delete</a>

urls.py

path('delete_song/<str:id>', views.delete_song, name='delete_song'),

views.py

def delete_song(request, id):
    song_to_delete = Song.objects.get(pk=id)
    song_to_delete.image.delete() # delete the file/image
    song_to_delete.delete() # finally delete the object
    return redirect('home')

0
此代码将在每次上传新图片(logo字段)时运行,并检查是否已存在标识,如果是,则关闭它并从磁盘中删除它。当然也可以在接收器函数中实现相同的过程。希望这能帮到你。
 #  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='')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接