Як змусити адміністратора Django видалити файли, коли я видаляю об’єкт із бази даних / моделі?


85

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


Хм, насправді так і повинно. Перевірте дозволи для файлів у папці для завантаження (змініть на 0777).
Торстен Енгельбрехт,

5
Django видалив функцію автоматичного видалення (для працівників Google, які бачать наведений вище коментар).
Позначте

Відповіді:


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)полів.
Antony Hatchkins

47
Дякую. Загалом, я б рекомендував використовувати post_deleteсигнал, оскільки він безпечніший у випадку невдалого видалення з будь-якої причини. Тоді ні модель, ні файл не буде видалено, зберігаючи дані послідовними. Будь ласка, виправте мене, якщо моє розуміння post_deleteта pre_deleteсигнали неправильні.
toto_tico

9
Зверніть увагу , що це не видаляє старий файл , якщо ви заміните файл на екземплярі моделі
Mark

3
У мене це не працює в Django 1.8 за межами адміністратора. Чи є новий спосіб це зробити?
халіф

дивовижний. шукав це довгий час
RL Shyam

46

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

pip install django-cleanup

settings.py

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

1
Дуже приголомшливий пакет. Дякую! :)
BoJack Horseman

3
Після обмеженого тестування я можу підтвердити, що цей пакет все ще працює для Django 1.10.
CoderGuy123

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-сховища), ця конкретна відповідь не буде працювати. Ви отримаєте a. NotImplementedError: This backend doesn't support absolute paths.Ви можете легко це виправити, передавши ім'я поля файлу storage.delete()замість шляху поля файлу. Наприклад, замініть останні два рядки цієї відповіді на storage, name = photo.original_image.storage, photo.original_image.namethen storage.delete(name).
Шон Азлін,

2
@Sean +1, я використовую це налаштування в 1.7, щоб видалити ескізи, створені django-imagekit на S3 через django-сховища. 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, коли модель видаляється, метод FileField delete () не буде викликаний. Якщо вам потрібно очищення застарілих файлів, ви" Потрібно обробити це самостійно (наприклад, за допомогою власної команди управління, яку можна запускати вручну або планувати періодичну роботу, наприклад, через cron). "
darrinm

4
Це рішення неправильне! deleteне завжди викликається при видаленні рядка, ви повинні використовувати сигнали.
lvella

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)

Чудове рішення!
AlexKh

6

Ви можете розглянути можливість використання сигналу pre_delete або post_delete:

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

Звичайно, ті самі причини, через які було видалено автоматичне видалення FileField, також застосовуються тут. Якщо ви видалите файл, на який є посилання в іншому місці, у вас будуть проблеми.

У моєму випадку це здавалося доречним, оскільки у мене була спеціальна модель файлів для управління всіма моїми файлами.

Примітка: З якихось причин post_delete, здається, не працює належним чином. Файл було видалено, але запис бази даних залишився, що абсолютно протилежне тому, що я очікував, навіть за умов помилки. pre_delete працює добре.


3
ймовірно post_delete, не буде працювати, тому що file_field.delete()за замовчуванням зберігає модель у базі даних, спробуйте file_field.delete(False) docs.djangoproject.com/en/1.3/ref/models/fields/…
Адам Юрчик

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 / signal.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. Я створив функцію, яка видаляє файли для об'єктів на основі, якщо файл, на який посилання на об'єкт не існує, видаліть об'єкт, якщо файл не має об'єкта, то також видаліть, також він може видалити на основі "активного" прапора для object .. Щось, що я додав до більшості своїх моделей. Вам потрібно передати йому об’єкти, які потрібно перевірити, шлях до файлів об’єктів, поле файлу та прапор для видалення неактивних об’єктів:

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

переконайтеся, що перед файлом пишете " self ". так приклад вище повинен бути

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

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

Я забув "self" перед своїм файлом, і це не спрацювало, оскільки воно виглядало у глобальному просторі імен.



0

Рішення Django 2.x:

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

Спочатку напишіть Custom Manager . Отже, якщо ви хочете мати можливість видаляти файли моделі за допомогою objectsметодів, ви повинні написати та використовувати [Спеціальний менеджер] [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)

1
Це дуже погана ідея. Ви не тільки видалите цілий каталог порівняно з одним файлом (що може вплинути на інші файли), але й у випадку, якщо фактичне видалення об’єкта не вдасться.
tbm

Це непогана ідея, якщо ви працювали над проблемою, яка у мене була;) Як я вже згадував, у мене був унікальний випадок використання, коли модель, що видаляється, була батьківською. Діти записували файли в батьківську папку, тому, якщо ви видалили батьківську, бажаною поведінкою було те, що всі файли в папці були видалені. Хороший момент щодо порядку операцій. Тоді мені це не спало на думку.
carruthd

Я все-таки вважаю за краще видалити окремі дочірні файли, коли дитина буде видалена; тоді, якщо вам потрібно, ви можете видалити батьківський каталог, коли він порожній.
tbm

Це має сенс, оскільки ви виймаєте дочірні об’єкти, але якщо батьківський об’єкт знищений, переходити через дітей по одному видається нудним і непотрібним. Незважаючи на це, зараз я бачу, що відповідь, яку я дав, була недостатньо конкретною на питання OP. Дякую за коментарі, ви змусили мене задуматись про використання менш тупого інструменту в майбутньому.
carruthd
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.