Django eliminar FileField

93

Estoy construyendo una aplicación web en Django. Tengo un modelo que sube un archivo, pero no puedo borrarlo. Aquí está mi código:

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)

Luego, en "python manage.py shell" hago esto:

song = Song.objects.get(pk=1)
song.delete()

Elimina de la base de datos pero no el archivo en el servidor. ¿Qué más puedo probar?

¡Gracias!

Marcos Aguayo
fuente
¿Qué hay de usar el default_storage directamente? docs.djangoproject.com/en/dev/topics/files
MGP

Respuestas:

141

Antes de Django 1.3, el archivo se eliminaba del sistema de archivos automáticamente cuando eliminaba la instancia del modelo correspondiente. Probablemente esté utilizando una versión más reciente de Django, por lo que tendrá que implementar la eliminación del archivo del sistema de archivos usted mismo.

Puede hacerlo de varias formas, una de las cuales es mediante una señal pre_deleteo post_delete.

Ejemplo

Mi método de elección actualmente es una combinación de post_deletey pre_saveseñales, lo que hace que los archivos obsoletos se eliminen cada vez que se eliminan los modelos correspondientes o se modifican sus archivos.

Basado en un MediaFilemodelo hipotético :

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)
  • Caso de borde: si su aplicación carga un nuevo archivo y apunta la instancia del modelo al nuevo archivo sin llamar save()(por ejemplo, mediante una actualización masiva a QuerySet), el archivo antiguo seguirá ahí porque las señales no se ejecutarán. Esto no sucede si utiliza métodos de manejo de archivos convencionales.
  • Creo que una de las aplicaciones que he creado tiene este código en producción pero, sin embargo, utilícelo bajo su propio riesgo.
  • Estilo de codificación: este ejemplo usa filecomo nombre de campo, que no es un buen estilo porque choca con el fileidentificador de objeto incorporado .

Ver también

  • FieldFile.delete()en la referencia de campo del modelo Django 1.11 (tenga en cuenta que describe la FieldFileclase, pero llamaría .delete()directamente en el campo: FileFieldproxy de instancia a la FieldFileinstancia correspondiente , y accede a sus métodos como si fueran de campo)

    Tenga en cuenta que cuando se elimina un modelo, los archivos relacionados no se eliminan. Si necesita limpiar archivos huérfanos, deberá manejarlo usted mismo (por ejemplo, con un comando de administración personalizado que puede ejecutarse manualmente o programarse para ejecutarse periódicamente a través de cron, por ejemplo).

  • Por qué Django no elimina archivos automáticamente: entrada en las notas de la versión para Django 1.3

    En versiones anteriores de Django, cuando se eliminaba una instancia de modelo que contenía a FileField, FileFieldse encargaba de eliminar también el archivo del almacenamiento de backend. Esto abrió la puerta a varios escenarios de pérdida de datos, incluidas transacciones y campos revertidos en diferentes modelos que hacen referencia al mismo archivo. En Django 1.3, cuando se elimina un modelo de la FileField's delete()no será llamado método. Si necesita limpiar archivos huérfanos, deberá manejarlo usted mismo (por ejemplo, con un comando de administración personalizado que se puede ejecutar manualmente o programar para ejecutarse periódicamente a través de cron, por ejemplo).

  • Ejemplo de uso de una pre_deletesola señal

Anton Strogonoff
fuente
2
Sí, pero asegúrese de realizar las comprobaciones adecuadas. (Dame un segundo, publicaré el código que encontré en uso en el sistema real.)
Anton Strogonoff
7
Probablemente sea mejor usarlo instance.song.delete(save=False), ya que usa el motor de almacenamiento correcto de django.
Eduardo
1
Es raro hoy en día que copie código. No habría podido escribirme directamente desde SO y funciona con modificaciones limitadas. ¡Fantástica ayuda, gracias!
GJStein
Encontré un error en esto donde si la instancia existe, pero no se guardó ninguna imagen previamente, os.path.isfile(old_file.path)falla porque old_file.pathgenera un error (no hay ningún archivo asociado con el campo). Lo arreglé agregando if old_file:justo antes de la llamada a os.path.isfile().
three_pineapples
@three_pineapples tiene sentido. Podría ser que la restricción NOT NULL en el campo del archivo se haya omitido o no se haya cerrado en algún momento, en cuyo caso algunos objetos lo tendrían vacío.
Anton Strogonoff
78

Pruebe django-cleanup , automáticamente invoca el método de eliminación en FileField cuando elimina el modelo.

pip install django-cleanup

settings.py

INSTALLED_APPS = (
     ...
    'django_cleanup', # should go after your apps
)
un1t
fuente
Genial, debe agregarse a FileField de forma predeterminada, ¡gracias!
megajoe
También está eliminando el archivo mientras se carga
chirag soni
Guau. Estaba tratando de que esto no sucediera y no podía entender por qué. Alguien lo había instalado hace años y lo había olvidado. Gracias.
ryan28561
4
Entonces, ¿por qué Django eliminó la función de eliminación de campos de archivos en primer lugar?
ha-neul
¡¡Eres la leyenda !!
marlonjd
32

Puede eliminar un archivo del sistema de archivos con el .deletemétodo de llamada del campo de archivo que se muestra a continuación con Django> = 1.10:

obj = Song.objects.get(pk=1)
obj.song.delete()
Mesut Tasci
fuente
7
Debe ser la respuesta aceptada, simple y simplemente funciona.
Nikolay Shindarov
14

También puede simplemente sobrescribir la función de eliminación del modelo para verificar el archivo si existe y eliminarlo antes de llamar a la superfunción.

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)
Shashank Singla
fuente
8
Tenga en cuenta que las llamadas queryset.delete()no limpiarán los archivos con esta solución. Debería iterar sobre el conjunto de consultas y llamar .delete()a cada objeto.
Scott Woodall
Soy nuevo en Django. Esto es bueno, pero ¿qué pasaría si el modelo heredara de una clase abstracta que haya anulado el método de eliminación? ¿No anularía esto el de la clase abstracta? El uso de señales me parece mejor
theTypan
8

Solución Django 2.x:

Es muy fácil manejar la eliminación de archivos en Django 2 . Intenté seguir la solución usando Django 2 y SFTP Storage y también FTP STORAGE, y estoy bastante seguro de que funcionará con cualquier otro administrador de almacenamiento que haya implementado el deletemétodo. (el deletemétodo es uno de los storagemétodos abstractos).

Anule el deletemétodo del modelo de forma que la instancia elimine sus FileFields antes de borrarse a sí misma:

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

Funciona bastante fácil para mí. Si desea verificar si el archivo existe antes de eliminarlo, puede usar storage.exists. por ejemplo self.song.storage.exists(self.song.name), devolverá una booleanrepresentación si la canción existe. Entonces se verá así:

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

EDITAR (además):

Como mencionó @HeyMan , con esta solución, la llamada Song.objects.all().delete()no elimina archivos. Esto sucede porque se Song.objects.all().delete()está ejecutando la consulta de eliminación del Administrador predeterminado . Entonces, si desea poder eliminar archivos de un modelo mediante el uso de objectsmétodos, debe escribir y usar un Administrador personalizado (solo para anular su consulta de eliminación):

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

y para asignar el CustomManageral modelo, debe poner sus iniciales objectsdentro de su modelo:

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

Ahora puede usar .delete()al final de cualquier objectssubconsulta. Escribí lo más simple CustomManager, pero puedes hacerlo mejor devolviendo algo sobre los objetos que eliminaste o lo que quieras.

Hamidreza
fuente
1
Sí, creo que agregaron esa característica desde que publiqué la pregunta.
Marcos Aguayo
1
Aún así, eliminar no se llama cuando se llama a Song.objects.all (). Delete (). Lo mismo ocurre cuando la instancia es eliminada por on_delete = models.CASCADE.
HeyMan
@HeyMan Lo resolví y edité mi solución ahora mismo :)
Hamidreza
4

Aquí hay una aplicación que eliminará archivos antiguos cada vez que se elimine el modelo o se cargue un archivo nuevo: 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)
lehins
fuente
3

@Anton Strogonoff

Me falta algo en el código cuando cambia un archivo, si crea un archivo nuevo genera un error, porque es un archivo nuevo y no encontró una ruta. Modifiqué el código de función y agregué una oración try / except y funciona bien.

@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
Desarrollador Java
fuente
No me he encontrado con esto, podría ser un error en mi código o algo cambiado en Django. Sin embargo, sugeriría capturar una excepción específica en su try:bloque ( AttributeError¿quizás?).
Anton Strogonoff
No es una buena idea usar la biblioteca del sistema operativo, ya que encontrará problemas si migra a un almacenamiento diferente (Amazon S3, por ejemplo).
Igor Pomaranskiy
@IgorPomaranskiy ¿Qué pasaría en un almacenamiento como Amazon S3 cuando usa os.remove?
Daniel González Fernández
@ DanielGonzálezFernández Supongo que fallará (con un error como algo sobre una ruta inexistente). Es por eso que Django usa abstracciones para almacenamientos.
Igor Pomaranskiy
0

Este código se ejecutará cada vez que subo una nueva imagen (campo del logotipo) y verifico si ya existe un logotipo, si es así, ciérrelo y elimínelo del disco. Por supuesto, se podría realizar el mismo procedimiento en la función de receptor. Espero que esto ayude.

 #  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='')
LanfeaR
fuente