¿Cómo consigo que Django Admin elimine archivos cuando elimino un objeto de la base de datos / modelo?

85

Estoy usando 1.2.5 con un ImageField estándar y usando el backend de almacenamiento incorporado. Los archivos se cargan bien, pero cuando elimino una entrada del administrador, el archivo real en el servidor no se elimina.

narkeeso
fuente
Hm, en realidad debería. Verifique los permisos de archivo en su carpeta de carga (cambie a 0777).
Torsten Engelbrecht
5
Django eliminó la función de eliminación automática (para los empleados de Google que ven el comentario anterior).
Mark

Respuestas:

100

Puede recibir la señal pre_deleteo post_delete(ver el comentario de @ toto_tico a continuación) y llamar al método delete () en el objeto FileField, así (en 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)
darrinm
fuente
10
Asegúrese de agregar una instance.filemarca de verificación si el campo no está vacío o puede (al menos intentarlo) eliminar todo el directorio MEDIA_ROOT. Esto se aplica incluso a los ImageField(null=False)campos.
Antony Hatchkins
47
Gracias. En general, recomendaría usar la post_deleteseñal porque es más seguro en caso de que la eliminación falle por cualquier motivo. Entonces ni el modelo, ni el archivo se eliminarían manteniendo los datos consistentes. Corríjame si mi comprensión de las señales post_deletey las pre_deleteseñales es incorrecta.
toto_tico
9
Tenga en cuenta que esto no elimina el archivo anterior si reemplaza el archivo en una instancia de modelo
Mark
3
Esto no me funciona en Django 1.8 fuera del administrador. ¿Existe una nueva forma de hacerlo?
califa
increíble. estaba buscando esto durante mucho tiempo
RL Shyam
46

Prueba django-cleanup

pip install django-cleanup

settings.py

INSTALLED_APPS = (
    ...
    'django_cleanup', # should go after your apps
)
un1t
fuente
1
Paquete muy impresionante. ¡Gracias! :)
BoJack Horseman
3
Después de pruebas limitadas, puedo confirmar que este paquete todavía funciona para Django 1.10.
CoderGuy123
1
Bien, esto es tan fácil
Tunn
Agradable. Funciona para mí en Django 2.0. También estoy usando S3 como mi backend de almacenamiento ( django-storages.readthedocs.io/en/latest/backends/… ) y felizmente está borrando archivos de S3.
routeburn
35

Solución Django 1.5: uso post_delete por varias razones internas de mi aplicación.

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)

Pegué esto en la parte inferior del archivo models.py.

el original_imagecampo es el ImageFieldde mi Photomodelo.

Kushal
fuente
7
Para cualquiera que use Amazon S3 como backend de almacenamiento (a través de django-storages), esta respuesta en particular no funcionará. Obtendrá un NotImplementedError: This backend doesn't support absolute paths.Puede solucionar esto fácilmente pasando el nombre del campo del archivo a en storage.delete()lugar de la ruta del campo del archivo. Por ejemplo, reemplace las dos últimas líneas de esta respuesta con storage, name = photo.original_image.storage, photo.original_image.nameentonces storage.delete(name).
Sean Azlin
2
@Sean +1, estoy usando ese ajuste en 1.7 para eliminar miniaturas generadas por django-imagekit en S3 a través de django-storages. docs.djangoproject.com/en/dev/ref/files/storage/… . Nota: Si simplemente está usando un ImageField (o FileField), puede usar mymodel.myimagefield.delete(save=False)en su lugar. docs.djangoproject.com/en/dev/ref/files/file/…
user2616836
@ user2616836 Se puede utilizar mymodel.myimagefield.delete(save=False)en post_delete? En otras palabras, puedo ver que puedo eliminar el archivo, pero ¿puedes eliminar el archivo cuando se elimina un modelo que tiene el campo de imagen?
eugene
1
@eugene Sí, puedes, funciona (aunque no estoy seguro de por qué). En post_deletelo hace instance.myimagefield.delete(save=False), tenga en cuenta el uso de instance.
user2616836
17

Este código funciona bien en Django 1.4 también con el panel de administración.

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)

Es importante obtener el almacenamiento y la ruta antes de eliminar el modelo o este último permanecerá vacío también si se elimina.

Davide Muzzarelli
fuente
3
Esto no funciona para mí (Django 1.5) y Django 1.3 CHANGELOG dice: "En Django 1.3, cuando se elimina un modelo, no se llamará al método delete () de FileField. Si necesita limpiar archivos huérfanos, Necesitará manejarlo usted mismo (por ejemplo, con un comando de administración personalizado que se puede ejecutar manualmente o programar para que se ejecute periódicamente a través de, por ejemplo, cron) ".
darrinm
4
¡Esta solución es incorrecta! deleteno siempre se llama cuando se elimina una fila, debe utilizar señales.
lvella
11

Usted necesita para eliminar el archivo real en tanto deletey 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
fuente
¡Gran solución!
AlexKh
6

Puede considerar usar una señal pre_delete o post_delete:

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

Por supuesto, las mismas razones por las que se eliminó la eliminación automática de FileField también se aplican aquí. Si elimina un archivo al que se hace referencia en otro lugar, tendrá problemas.

En mi caso, esto parecía apropiado porque tenía un modelo de archivo dedicado para administrar todos mis archivos.

Nota: Por alguna razón, post_delete no parece funcionar correctamente. El archivo se eliminó, pero el registro de la base de datos se mantuvo, lo cual es completamente lo contrario de lo que esperaría, incluso en condiciones de error. pre_delete funciona bien.

SystemParadox
fuente
3
probablemente post_deleteno funcione, porque file_field.delete()de forma predeterminada guarda el modelo en la base de datos, pruebe file_field.delete(False) docs.djangoproject.com/en/1.3/ref/models/fields/…
Adam Jurczyk
3

Quizás sea un poco tarde. Pero la forma más fácil para mí es usar una señal post_save. Solo para recordar que las señales se ejecutan incluso durante un proceso de eliminación de QuerySet, pero el método [modelo] .delete () no se ejecuta durante el proceso de eliminación de QuerySet, por lo que no es la mejor opción para anularlo.

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
Mauricio
fuente
1

Esta funcionalidad se eliminará en Django 1.3, por lo que no confiaría en ella.

Puede anular el deletemétodo del modelo en cuestión para eliminar el archivo antes de eliminar completamente la entrada de la base de datos.

Editar:

He aquí un ejemplo rápido.

class MyModel(models.Model):

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

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

        super(MyModel, self).delete(*args, **kwargs)
Derek Reynolds
fuente
¿Tiene un ejemplo de cómo usar eso en un modelo para eliminar el archivo? Estoy mirando los documentos y veo ejemplos de cómo eliminar el objeto de la base de datos, pero no veo ninguna implementación en la eliminación de archivos.
narkeeso
2
Este método es incorrecto porque no funcionará para la eliminación masiva (como la función 'Eliminar seleccionados' del administrador). Por ejemplo MyModel.objects.all()[0].delete(), eliminará el archivo mientras MyModel.objects.all().delete()que no. Usa señales.
Antony Hatchkins
1

Usar post_delete es sin duda el camino correcto. A veces, las cosas pueden salir mal y los archivos no se eliminan. Por supuesto, existe el caso de que tenga un montón de archivos antiguos que no se eliminaron antes de que se usara post_delete. Creé una función que elimina archivos para objetos en función de si el archivo al que hace referencia el objeto no existe, luego elimine el objeto, si el archivo no tiene un objeto, luego también elimine, también puede eliminar basado en una bandera "activa" para un objeto .. Algo que agregué a la mayoría de mis modelos. Tienes que pasarle los objetos que quieres comprobar, la ruta a los archivos de objetos, el campo del archivo y una bandera para borrar los objetos inactivos:

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."

Así es como puedes usar esto:

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
fuente
0

asegúrese de escribir " self " antes del archivo. así que el ejemplo anterior debería ser

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

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

He olvidado el "yo" antes de mi archivo y eso no funcionó ya que estaba buscando en el espacio de nombres global.

Bjorn
fuente
0

Solución Django 2.x:

¡No es necesario instalar ningún paquete! Es muy fácil de manejar en Django 2 . Intenté seguir la solución usando Django 2 y SFTP Storage (sin embargo, creo que funcionaría con cualquier almacenamiento)

Primero escriba un administrador personalizado . Por lo tanto, si desea poder eliminar archivos de un modelo mediante objectsmétodos, debe escribir y usar un [Administrador personalizado] [3] (para reemplazar el delete()método de objects):

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

Ahora debe eliminar imageantes de eliminar eliminando el modelo en sí y para asignar el CustomManageral modelo, debe poner sus iniciales objectsdentro de su modelo:

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()
Hamidreza
fuente
-1

Es posible que tenga un caso especial ya que estoy usando la opción upload_to en mi campo de archivo con nombres de directorio dinámico, pero la solución que encontré fue usar os.rmdir.

En modelos:

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
fuente
1
Esta es una muy mala idea. No solo eliminará un directorio completo frente a un solo archivo (que podría afectar a otros archivos), sino que lo hará incluso si falla la eliminación del objeto real.
tbm
No es una mala idea si estaba trabajando en el problema que tuve;) Como mencioné, tuve un caso de uso único en el que el modelo que se estaba eliminando era un modelo principal. Los niños escribieron archivos en la carpeta principal y, por lo tanto, si eliminaba la carpeta principal, el comportamiento deseado era que se eliminaran todos los archivos de la carpeta. Sin embargo, es un buen punto sobre el orden de las operaciones. Eso no se me ocurrió en ese momento.
carruthd
Aún prefiero eliminar los archivos secundarios individuales cuando se elimina un niño; luego, si es necesario, puede eliminar el directorio principal cuando esté vacío.
tbm
Eso tiene sentido ya que está sacando objetos secundarios, pero si el objeto principal se destruye, pasar a través de los niños uno a la vez parece tedioso e innecesario. Independientemente, ahora veo que la respuesta que di no fue lo suficientemente específica para la pregunta de OP. Gracias por los comentarios, me hiciste pensar en usar un instrumento menos contundente en el futuro.
carruthd