当我从数据库/模型中删除一个对象时,如何让Django管理员删除文件?

94

我正在使用1.2.5版本,使用标准的ImageField并使用内置的存储后端。文件上传正常,但当我从管理员中删除一个条目时,实际上服务器上的文件并没有被删除。


嗯,实际上应该可以。检查您上传文件夹的文件权限(更改为0777)。 - Torsten Engelbrecht
7
Django移除了自动删除功能(针对看到上面评论的谷歌员工)。 - Mark
13个回答

110
你可以接收pre_deletepost_delete信号(参见下面 @toto_tico 的评论),并在 FileField 对象上调用 delete() 方法,因此(在 models.py 中):
class MyModel(models.Model):
    file = models.FileField()
    ...

# Receive the pre_delete signal and delete the file associated with the model instance.
from django.db.models.signals import pre_delete
from django.dispatch.dispatcher import receiver

@receiver(pre_delete, sender=MyModel)
def mymodel_delete(sender, instance, **kwargs):
    # Pass false so FileField doesn't save the model.
    instance.file.delete(False)

11
请确保在删除MEDIA_ROOT目录之前,检查instance.file字段是否为空。即使是ImageField(null=False)字段也要这样做。如果该字段不为空,则不要删除整个目录,以免发生意外情况。 - Antony Hatchkins
51
一般而言,我建议使用 post_delete 信号,因为在删除失败的情况下,这样更安全。这样既不会删除模型也不会删除文件,保持数据的一致性。如果我对 post_deletepre_delete 信号的理解有误,请纠正我。 - toto_tico
9
请注意,如果您替换模型实例上的文件,这不会删除旧文件。 - Mark
3
在 Django 1.8 版本中,此方法在管理后台之外无法使用。是否有新的方法可以实现? - caliph
它在Django 1.11中可以工作。您应该使用pre_delete信号,post_delete不起作用。 - Max Malysh
显示剩余2条评论

58

尝试 django-cleanup

pip install django-cleanup

设置.py

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

4
经过有限的测试,我可以确认这个包仍然适用于 Django 1.10 版本。 - CoderGuy123
1
不错。在Django 2.0上对我有用。我还使用S3作为我的存储后端(http://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html),并且它可以愉快地从S3中删除文件。 - routeburn
1
正在开发 Django 4.x。 - Genarito

35

Django 1.5的解决方案:出于我应用程序内部的各种原因,我使用post_delete。

from django.db.models.signals import post_delete
from django.dispatch import receiver

@receiver(post_delete, sender=Photo)
def photo_post_delete_handler(sender, **kwargs):
    photo = kwargs['instance']
    storage, path = photo.original_image.storage, photo.original_image.path
    storage.delete(path)

我把它放在models.py文件的底部。

original_image字段是我Photo模型中的ImageField


8
如果您使用Amazon S3作为存储后端(通过django-storages),那么这个特定的答案不适用。您将会得到一个NotImplementedError: This backend doesn't support absolute paths.的错误。您可以轻松地通过将文件字段的名称传递给storage.delete()来解决此问题,而不是文件字段的路径。例如,将此答案的最后两行替换为 storage, name = photo.original_image.storage, photo.original_image.name 然后 storage.delete(name) - Sean Azlin
2
@Sean +1,我正在使用1.7中的调整来通过django-storages删除S3上由django-imagekit生成的缩略图。https://docs.djangoproject.com/en/dev/ref/files/storage/#the-storage-class。注意:如果您只是使用ImageField(或FileField),则可以改用`mymodel.myimagefield.delete(save=False)`。https://docs.djangoproject.com/en/dev/ref/files/file/#additional-methods-on-files-attached-to-objects - user2616836
@user2616836,你能在post_delete中使用mymodel.myimagefield.delete(save=False)吗?换句话说,我知道可以删除文件,但是当包含imagefield的模型被删除时,你能否删除文件呢? - eugene
1
@eugene 是的,你可以这样做,它是有效的(虽然我不确定为什么)。在 post_delete 中,你需要使用 instance,并执行 instance.myimagefield.delete(save=False) - user2616836

18

这段代码在Django 1.4上也能够正常运行,并且可以与管理面板一起使用。

class ImageModel(models.Model):
    image = ImageField(...)

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

在删除模型之前获取存储和路径非常重要,否则即使已被删除,后者也将保留。


3
这对我没有用(Django 1.5),而 Django 1.3 的变更日志中指出:“在Django 1.3中,当模型被删除时,不会调用FileField的delete()方法。如果您需要清理孤立文件,您需要自己处理(例如,使用自定义管理命令,可以手动运行或定期运行,例如通过cron)。" - darrinm
4
这个解决方案是错误的!delete 不总是在行被删除时被调用,你必须使用信号。 - lvella

17

删除更新时,您需要删除实际文件。

from django.db import models

class MyImageModel(models.Model):
    image = models.ImageField(upload_to='images')

    def remove_on_image_update(self):
        try:
            # is the object in the database yet?
            obj = MyImageModel.objects.get(id=self.id)
        except MyImageModel.DoesNotExist:
            # object is not in db, nothing to worry about
            return
        # is the save due to an update of the actual image file?
        if obj.image and self.image and obj.image != self.image:
            # delete the old image file from the storage in favor of the new file
            obj.image.delete()

    def delete(self, *args, **kwargs):
        # object is being removed from db, remove the file from storage first
        self.image.delete()
        return super(MyImageModel, self).delete(*args, **kwargs)

    def save(self, *args, **kwargs):
        # object is possibly being updated, if so, clean up.
        self.remove_on_image_update()
        return super(MyImageModel, self).save(*args, **kwargs)

很棒的解决方案! - AlexKh

6
您可以考虑使用pre_delete或post_delete信号:

https://docs.djangoproject.com/en/dev/topics/signals/

当然,删除FileField自动删除功能被删除的原因同样适用于这里。如果您删除了在其他地方被引用的文件,就会出现问题。
在我的情况下,这似乎是合适的,因为我有一个专门的File模型来管理所有的文件。
注意:由于某种原因,post_delete似乎无法正常工作。文件已被删除,但数据库记录仍然存在,这完全与我预期的相反,即使在错误条件下也是如此。pre_delete却可以很好地工作。

3
可能 post_delete 不起作用,因为 file_field.delete() 默认会将模型保存到数据库中,请尝试使用 file_field.delete(False)。更多信息请参阅 https://docs.djangoproject.com/en/1.3/ref/models/fields/#django.db.models.FieldFile.delete。 - Adam Jurczyk

3
也许有点晚了。但对我来说,最简单的方法是使用post_save信号。请记住,即使在QuerySet删除过程中,信号也会被执行,但[model].delete()方法不会在QuerySet删除过程中被执行,因此覆盖它并不是最好的选择。
core/models.py:
from django.db import models
from django.db.models.signals import post_delete
from core.signals import delete_image_slide
SLIDE1_IMGS = 'slide1_imgs/'

class Slide1(models.Model):
    title = models.CharField(max_length = 200)
    description = models.CharField(max_length = 200)
    image = models.ImageField(upload_to = SLIDE1_IMGS, null = True, blank = True)
    video_embed = models.TextField(null = True, blank = True)
    enabled = models.BooleanField(default = True)

"""---------------------------- SLIDE 1 -------------------------------------"""
post_delete.connect(delete_image_slide, Slide1)
"""--------------------------------------------------------------------------"""

core/signals.py

import os

def delete_image_slide(sender, **kwargs):
    slide = kwargs.get('instance')
    try:
        os.remove(slide.image.path)
    except:
        pass

1

这个功能将在Django 1.3中被移除,所以我不会依赖它。

您可以重写相关模型的delete方法,在完全从数据库中删除条目之前删除文件。

编辑:

这里是一个快速的示例。

class MyModel(models.Model):

    self.somefile = models.FileField(...)

    def delete(self, *args, **kwargs):
        somefile.delete()

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

你有如何在模型中使用它以删除文件的示例吗?我正在查看文档,看到了如何从数据库中删除对象的示例,但没有看到任何关于文件删除的实现。 - narkeeso
2
这种方法是错误的,因为它无法用于批量删除(例如管理员的“删除所选”功能)。例如,MyModel.objects.all()[0].delete()将删除文件,而MyModel.objects.all().delete()则不会。请使用信号。 - Antony Hatchkins

1
使用post_delete是正确的方法。但有时会出现问题,文件无法被删除。当然,还有一种情况是在使用post_delete之前就有很多旧文件没有被删除。我创建了一个函数,根据对象引用的文件是否存在来删除对象的文件,如果文件没有对象引用,则同时删除文件,还可以根据对象的“活动”标志进行删除。这是我添加到大多数模型中的内容。您需要传递要检查的对象、对象文件的路径、文件字段和删除非活动对象的标志。
def cleanup_model_objects(m_objects, model_path, file_field='image', clear_inactive=False):
    # PART 1 ------------------------- INVALID OBJECTS
    #Creates photo_file list based on photo path, takes all files there
    model_path_list = os.listdir(model_path)

    #Gets photo image path for each photo object
    model_files = list()
    invalid_files = list()
    valid_files = list()
    for obj in m_objects:

        exec("f = ntpath.basename(obj." + file_field + ".path)")  # select the appropriate file/image field

        model_files.append(f)  # Checks for valid and invalid objects (using file path)
        if f not in model_path_list:
            invalid_files.append(f)
            obj.delete()
        else:
            valid_files.append(f)

    print "Total objects", len(model_files)
    print "Valid objects:", len(valid_files)
    print "Objects without file deleted:", len(invalid_files)

    # PART 2 ------------------------- INVALID FILES
    print "Files in model file path:", len(model_path_list)

    #Checks for valid and invalid files
    invalid_files = list()
    valid_files = list()
    for f in model_path_list:
        if f not in model_files:
            invalid_files.append(f)
        else:
            valid_files.append(f)
    print "Valid files:", len(valid_files)
    print "Files without model object to delete:", len(invalid_files)

    for f in invalid_files:
        os.unlink(os.path.join(model_path, f))

    # PART 3 ------------------------- INACTIVE PHOTOS
    if clear_inactive:
        #inactive_photos = Photo.objects.filter(active=False)
        inactive_objects = m_objects.filter(active=False)
        print "Inactive Objects to Delete:", inactive_objects.count()
        for obj in inactive_objects:
            obj.delete()
    print "Done cleaning model."

这是如何使用它的:

photos = Photo.objects.all()
photos_path, tail = ntpath.split(photos[0].image.path)  # Gets dir of photos path, this may be different for you
print "Photos -------------->"
cleanup_model_objects(photos, photos_path, file_field='image', clear_inactive=False)  # image file is default

0
如果您的项目中已经有许多未使用的文件并且想要删除它们,您可以使用 Django 实用程序 django-unused-media

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