Django删除FileField


91

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

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

谢谢!


直接使用default_storage怎么办?docs.djangoproject.com/en/dev/topics/files
MGP

Answers:


140

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

您可以通过几种方式做到这一点,其中一种方式是使用pre_deletepost_delete信号。

我目前选择的方法是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()(例如,通过批量更新QuerySet),则旧文件将继续散布,因为将不会运行信号。如果使用常规文件处理方法,则不会发生这种情况。
  • 我认为我构建的其中一个应用程序已在生产中使用此代码,但使用时后果自负。
  • 编码样式:此示例将其file用作字段名称,但这不是一个好的样式,因为它与内置file对象标识符冲突。

也可以看看

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

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

  • 为什么Django无法自动删除文件:Django 1.3发行说明中的​​条目

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

  • pre_delete仅使用信号的示例


2
是的,但是请确保进行适当的检查。(请
稍等

7
最好使用instance.song.delete(save=False),因为它使用正确的django存储引擎。
Eduardo

1
如今,我很少复制代码,因此无法直接从SO编写自己的代码,并且修改后的代码很少。很棒的帮助,谢谢!
GJStein

在该实例中发现了一个错误,如果该实例存在,但是以前没有保存任何图像,则os.path.isfile(old_file.path)由于old_file.path引发错误(没有文件与该字段关联)而失败。我通过if old_file:在调用之前添加来修复它os.path.isfile()
three_pineapples's

@three_pineapples很有意义。可能是文件字段上的NOT NULL约束被绕过或在某个时候没有退出,在这种情况下,某些对象会将其为空。
安东·斯特罗戈诺夫

77

尝试django-cleanup,当您删除模型时,它将自动在FileField上调用delete方法。

pip install django-cleanup

settings.py

INSTALLED_APPS = (
     ...
    'django_cleanup', # should go after your apps
)

很酷,默认情况下需要将其添加到FileField中,谢谢!
megajoe

它也在上传时删除文件
chirag soni

哇。我试图使这种情况不发生,我不知道为什么会这样。几年前有人安装并忘记了它。谢谢。
ryan28561 '19

4
那么,为什么Django首先删除了filefield delete函数?
ha-neul

你是传奇!
marlonjd


14

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

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)

8
请注意,queryset.delete()使用此解决方案将无法清理文件。您将需要遍历queryset并调用.delete()每个对象。
Scott Woodall

我是Django的新手。这样做很好,但是如果模型是从覆盖了delete方法的抽象类继承的,这会不会覆盖抽象类的继承呢?使用信号对我来说似乎更好
–Typan

8

Django 2.x解决方案:

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

delete以实例在删除自身之前删除其FileField的方式来覆盖模型的方法:

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.song.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.song.name)

    super().delete()

编辑(另外):

正如@HeyMan提到的那样,使用此解决方案调用Song.objects.all().delete()不会删除文件!发生这种情况是因为Song.objects.all().delete()正在运行Default Manager的删除查询。因此,如果您希望能够使用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.song.name)
        super().delete()

现在,您可以.delete()objects任何子查询的末尾使用。我写了最简单的书CustomManager,但可以通过返回有关已删除对象或所需内容的信息来做得更好。


1
是的,我认为自我发布问题以来,他们就添加了该功能。
马科斯·阿瓜约

1
仍然删除不称为wenn调用Song.objects.all()。delete()。与on_delete = models.CASCADE删除实例时相同。
HeyMan

@HeyMan我已解决它并立即编辑我的解决方案:)
Hamidreza

4

这是一个可以在删除模型或上传新文件时删除旧文件的应用程序: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)

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

我还没有遇到过-这可能是我的代码中的错误,或者是Django中的某些更改。我建议在您的try:块中捕获特定的异常,不过(AttributeError也许?)。
Anton Strogonoff 2013年

使用os库不是一个好主意,因为如果迁移到其他存储(例如Amazon S3),则会遇到问题。
伊戈尔·波马兰斯基

@IgorPomaranskiy当您使用os.remove时,在像Amazon S3这样的存储中会发生什么?
丹尼尔·冈萨雷斯·费尔南德斯

@DanielGonzálezFernández我猜它会失败(出现类似不存在路径的错误)。这就是Django使用抽象存储的原因。
Igor Pomaranskiy

0

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

 #  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='')
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.