我正在 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 1.3 之前,当您删除相应的模型实例时,该文件会自动从文件系统中删除。您可能使用的是较新的 Django 版本,因此您必须自己实现从文件系统中删除文件。
简单的基于信号的样本
在撰写本文时,我选择的方法是混合使用 post_delete
和 pre_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() 方法调用恰好在回滚的事务中,您的数据最终可能会引用一个不存在的文件。您可以考虑按照 Mikhail 评论中的建议,按照 transaction.on_commit(lambda: os.remove(old_file.path)) 的行将文件删除逻辑包装到 transaction.on_commit() 中。 django-cleanup 库在这些方面做了一些事情。
边缘情况:如果您的应用上传新文件并将模型实例指向新文件而不调用 save()(例如通过批量更新 QuerySet),则旧文件将继续存在,因为不会运行信号。如果您使用传统的文件处理方法,则不会发生这种情况。
编码风格:本例使用文件作为字段名,这不是一个好的风格,因为它与内置的文件对象标识符冲突。
附录:定期清理
实际上,您可能还希望运行定期任务来处理孤立文件清理,以防运行时故障阻止某些文件被删除。考虑到这一点,您可能可以完全摆脱信号处理程序,并让这样的任务成为处理不敏感数据和不太大文件的机制。
但是,无论哪种方式,如果您正在处理敏感数据,最好进行双重或三重检查,确保您永远不会及时删除生产中的数据,以避免任何相关的责任。
也可以看看
Django 1.11 模型字段参考中的 FieldFile.delete() (请注意,它描述了 FieldFile 类,但您可以直接在字段上调用 .delete() : FileField 实例代理到相应的 FieldFile 实例,并且您访问它的方法就像它们是字段的)请注意,删除模型时,不会删除相关文件。如果您需要清理孤立的文件,您需要自己处理(例如,使用可以手动运行的自定义管理命令或通过例如 cron 计划定期运行)。
为什么 Django 不自动删除文件:Django 1.3 发行说明中的条目 在早期的 Django 版本中,当删除包含 FileField 的模型实例时,FileField 会自行从后端存储中删除文件。这为几种数据丢失场景打开了大门,包括回滚事务和引用同一文件的不同模型上的字段。在 Django 1.3 中,删除模型时不会调用 FileField 的 delete() 方法。如果您需要清理孤立文件,您需要自己处理(例如,使用可以手动运行或计划通过例如 cron 定期运行的自定义管理命令)。
仅使用 pre_delete 信号的示例
试试 django-cleanup,当您删除模型时,它会自动调用 FileField 上的 delete 方法。
pip install django-cleanup
设置.py
INSTALLED_APPS = (
...
'django_cleanup.apps.CleanupConfig',
)
您可以使用 Django >= 1.10 调用文件字段的 .delete
方法从文件系统中删除文件,如下所示:
obj = Song.objects.get(pk=1)
obj.song.delete()
views.py
还是 models.py
?
Django 2.x 解决方案:
在 Django 2 中处理文件删除非常容易。我尝试过使用 Django 2 和 SFTP Storage 以及 FTP STORAGE 的以下解决方案,我很确定它可以与任何其他实现 delete
方法的存储管理器一起使用。 (delete
方法是 storage
抽象方法之一,它应该从存储中物理删除文件!)
以实例在删除自身之前删除其 FileFields 的方式覆盖模型的 delete
方法:
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
,但您可以通过返回有关您删除的对象或任何您想要的内容来做得更好。
QuerySet
上调用 delete()
时,这个仍然没有调用 delete。根据文档,即使在 Django 3 中,也必须实现 post_delete
信号才能完全实现。
CustomManager
?我会检查 Django 3 并更新答案,tnx 提到:)
您也可以简单地覆盖模型的删除函数以检查文件是否存在并在调用超级函数之前将其删除。
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-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)
QuerySet
s 时不会。也许django-cleanup
?
@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
try:
块中捕获特定异常(可能是 AttributeError
?)。
对于那些在较新版本的 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)
每次我上传新图像(徽标字段)时都会运行此代码,并检查徽标是否已经存在,如果存在,请将其关闭并将其从磁盘中删除。当然,在接收器功能中也可以进行相同的过程。希望这可以帮助。
# 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='')
不定期副业成功案例分享
instance.song.delete(save=False)
可能会更好,因为它使用正确的 django 存储引擎。transaction.on_commit(lambda: os.remove(old_file.path))
,因为如果您删除一个文件,然后发生事务回滚,您将丢失一个文件django-cleanup
似乎做了类似的事情。