Как мне заставить Django Admin удалять файлы, когда я удаляю объект из базы данных / модели?

85

Я использую 1.2.5 со стандартным ImageField и использую встроенную систему хранения. Файлы загружаются нормально, но когда я удаляю запись от администратора, фактический файл на сервере не удаляется.

Наркисо
источник
Хм, на самом деле должно. Проверьте права доступа к файлам в папке загрузки (измените на 0777).
Торстен Энгельбрехт
5
Django удалил функцию автоматического удаления (для гуглеров, которые видят комментарий выше).
Марк

Ответы:

100

Вы можете получить сигнал pre_deleteили post_delete(см. Комментарий @ toto_tico ниже) и вызвать метод delete () для объекта FileField, таким образом (в 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)
Дарринм
источник
10
Обязательно добавьте проверку, если instance.fileполе не пустое или оно может (по крайней мере, попытаться) удалить весь каталог MEDIA_ROOT. Это касается даже ImageField(null=False)полей.
Энтони Хэтчкинс
47
Спасибо. В общем, я бы рекомендовал использовать post_deleteсигнал, потому что это безопаснее в случае, если удаление не удастся по какой-либо причине. Тогда ни модель, ни файл не будут удалены, сохраняя согласованность данных. Пожалуйста, поправьте меня, если я неправильно понимаю post_deleteи pre_deleteсигналы.
toto_tico 08
9
Обратите внимание, что это не удаляет старый файл, если вы заменяете файл в экземпляре модели
Марк
3
Это не работает для меня в Django 1.8 вне администратора. Есть новый способ сделать это?
Калиф
классно. долго искал это
Р.Л.
46

Попробуйте django-cleanup

pip install django-cleanup

settings.py

INSTALLED_APPS = (
    ...
    'django_cleanup', # should go after your apps
)
un1t
источник
1
Очень классная упаковка. Спасибо! :)
Конь БоДжек
3
После ограниченного тестирования я могу подтвердить, что этот пакет по-прежнему работает с Django 1.10.
CoderGuy123 08
1
Отлично
Ницца. У меня работает на Django 2.0. Я также использую S3 в качестве серверной части хранилища ( django-storages.readthedocs.io/en/latest/backends/… ), и он успешно удаляет файлы с S3.
routeburn
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поле является ImageFieldв моей Photoмодели.

Кушал
источник
7
Для тех, кто использует Amazon S3 в качестве серверной части хранилища (через django-storerages), этот конкретный ответ не сработает. Вы получите сообщение NotImplementedError: This backend doesn't support absolute paths.Вы можете легко исправить это, передав имя поля файла storage.delete()вместо пути к полю файла. Например, замените последние две строки этого ответа на storage, name = photo.original_image.storage, photo.original_image.nameзатем storage.delete(name).
Шон Азлин
2
@Sean +1, я использую эту настройку в 1.7 для удаления эскизов, созданных django-imagekit на S3 через django-storerages. docs.djangoproject.com/en/dev/ref/files/storage/… . Примечание. Если вы просто используете ImageField (или FileField), вы можете использовать mymodel.myimagefield.delete(save=False)вместо него. docs.djangoproject.com/en/dev/ref/files/file/…
user2616836
@ user2616836 Можно использовать mymodel.myimagefield.delete(save=False)на post_delete? Другими словами, я вижу, что могу удалить файл, но можете ли вы удалить файл, когда удаляется модель, имеющая поле изображения?
Евгений
1
@eugene Да, можно, это работает (хотя я не уверен, почему). В post_deleteвас instance.myimagefield.delete(save=False), обратите внимание на использование instance.
user2616836
17

Этот код хорошо работает на 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 CHANGELOG говорится: «В Django 1.3 при удалении модели метод delete () FileField не будет вызываться. Если вам нужно очистить потерянные файлы, вы» Мне нужно будет справиться с этим самостоятельно (например, с помощью специальной команды управления, которую можно запускать вручную или по расписанию, например, через cron) ».
darrinm
4
Это неправильное решение! deleteне всегда вызывается при удалении строки, необходимо использовать сигналы.
lvella 06
11

Вам необходимо удалить фактический файл как на, так deleteи на update.

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)
un33k
источник
Отличное решение!
AlexKh
6

Вы можете рассмотреть возможность использования сигнала pre_delete или post_delete:

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

Конечно, здесь применимы те же причины, по которым было удалено автоматическое удаление FileField. Если вы удалите файл, на который есть ссылка в другом месте, у вас возникнут проблемы.

В моем случае это казалось подходящим, потому что у меня была выделенная файловая модель для управления всеми моими файлами.

Примечание. По какой-то причине post_delete не работает правильно. Файл был удален, но запись в базе данных осталась, что совершенно противоположно тому, что я ожидал, даже в условиях ошибки. pre_delete работает нормально.

SystemParadox
источник
3
вероятно post_delete, не сработает, потому что file_field.delete()по умолчанию модель сохраняет в db, попробуйте file_field.delete(False) docs.djangoproject.com/en/1.3/ref/models/fields/…
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)
"""--------------------------------------------------------------------------"""

ядро / сигналы.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()не будет. Используйте сигналы.
Энтони Хэтчкинс
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
Radtek
источник
0

убедитесь, что вы написали " self " перед файлом. поэтому пример выше должен быть

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

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

Я забыл "я" перед моим файлом, и это не сработало, поскольку оно искало в глобальном пространстве имен.

Bjorn
источник
0

Если у вас уже есть несколько неиспользуемых файлов в вашем проекте и вы хотите их удалить, вы можете использовать утилиту django django-unused-media

Андрей Колпаков
источник
0

Решение Django 2.x:

Никаких пакетов устанавливать не нужно! В Django 2 это очень просто . Я пробовал следующее решение с использованием Django 2 и SFTP Storage (однако я думаю, что оно будет работать с любыми хранилищами)

Сначала напишите Custom Manager . Поэтому, если вы хотите иметь возможность удалять файлы модели с помощью objectsметодов, вы должны написать и использовать [Custom Manager] [3] (для delete()метода переопределения objects):

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

Теперь вы должны удалить imageперед удалением, удалив саму модель и для назначения CustomManagerмодели, вы должны инициализировать objectsвнутри своей модели:

class MyModel(models.Model):
    image = models.ImageField(upload_to='/pictures/', blank=True)
    objects = CustomManager() # add CustomManager to model
    def delete(self, using=None, keep_parents=False):

    objects = CustomManager() # just add this line of code inside of your model

    def delete(self, using=None, keep_parents=False):
        self.image.storage.delete(self.song.name)
        super().delete()
Хамидреза
источник
-1

У меня может быть особый случай, поскольку я использую опцию upload_to в своем поле файла с динамическими именами каталогов, но решение, которое я нашел, заключалось в использовании os.rmdir.

В моделях:

import os

...

class Some_Model(models.Model):
     save_path = models.CharField(max_length=50)
     ...
     def delete(self, *args,**kwargs):
          os.rmdir(os.path.join(settings.MEDIA_ROOT, self.save_path)
          super(Some_Model,self).delete(*args, **kwargs)
Carruthd
источник
1
Это очень плохая идея. Вы не только удалите весь каталог, а не один файл (что может повлиять на другие файлы), но и сделаете это, даже если фактическое удаление объекта не удастся.
tbm
Это неплохая идея, если вы работали над моей проблемой;) Как я уже упоминал, у меня был уникальный вариант использования, когда удаляемая модель была родительской. Потомки записывали файлы в родительскую папку, поэтому, если вы удалили родительскую папку, желаемое поведение заключалось в том, что все файлы в папке были удалены. Тем не менее, хорошее замечание по порядку действий. В то время мне этого не приходило в голову.
carruthd
Я по-прежнему предпочитаю удалять отдельные дочерние файлы при удалении ребенка; тогда, если вам нужно, вы можете удалить родительский каталог, когда он пуст.
tbm
Это имеет смысл, поскольку вы удаляете дочерние объекты, но если родительский объект уничтожен, переход через дочерние объекты по одному кажется утомительным и ненужным. Тем не менее, теперь я вижу, что ответ, который я дал, не был достаточно конкретным на вопрос OP. Спасибо за комментарии, вы заставили меня задуматься об использовании менее резкого инструмента в будущем.
carruthd