Come posso fare in modo che Django Admin elimini i file quando rimuovo un oggetto dal database / modello?


86

Sto usando 1.2.5 con un ImageField standard e utilizzo il backend di archiviazione integrato. I file vengono caricati correttamente, ma quando rimuovo una voce dall'amministratore il file effettivo sul server non viene eliminato.


Hm, in realtà dovrebbe. Controlla i permessi dei file sulla cartella di caricamento (cambia in 0777).
Torsten Engelbrecht

5
Django ha rimosso la funzione di eliminazione automatica (per i googler che vedono il commento sopra).
Mark

Risposte:


102

Puoi ricevere il segnale pre_deleteo post_delete(vedi il commento di @ toto_tico di seguito) e chiamare il metodo delete () sull'oggetto FileField, quindi (in 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)

11
Assicurati di aggiungere un segno di spunta se il instance.filecampo non è vuoto o può (almeno provare) a eliminare l'intera directory MEDIA_ROOT. Questo vale anche per i ImageField(null=False)campi.
Antony Hatchkins

49
Grazie. In generale, consiglierei di utilizzare il post_deletesegnale perché è più sicuro nel caso in cui l'eliminazione fallisca per qualsiasi motivo. Quindi né il modello, né il file verrebbero cancellati mantenendo i dati coerenti. Per favore correggimi se la mia comprensione post_deletee i miei pre_deletesegnali sono sbagliati.
toto_tico

9
Tieni presente che ciò non elimina il vecchio file se sostituisci il file su un'istanza del modello
Contrassegna il

3
Questo non funziona per me in Django 1.8 al di fuori dell'amministratore. C'è un nuovo modo per farlo?
califfo

eccezionale. stava cercando questo per molto tempo
RL Shyam

46

Prova django-cleanup

pip install django-cleanup

settings.py

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

1
Pacchetto davvero fantastico. Grazie! :)
BoJack Horseman

3
Dopo un test limitato, posso confermare che questo pacchetto funziona ancora per Django 1.10.
CoderGuy123

1
Bene, è così facile
Tunn

Bello. Funziona per me su Django 2.0. Sto anche usando S3 come backend di archiviazione ( django-storages.readthedocs.io/en/latest/backends/… ) e sto cancellando felicemente i file da S3.
rotta del

35

Soluzione Django 1.5: utilizzo post_delete per vari motivi interni alla mia app.

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)

L'ho bloccato in fondo al file models.py.

il original_imagecampo è ImageFieldnel mio Photomodello.


7
Per chiunque utilizzi Amazon S3 come backend di archiviazione (tramite django-storages), questa particolare risposta non funzionerà. Otterrai un NotImplementedError: This backend doesn't support absolute paths.Puoi facilmente risolvere questo problema passando il nome del campo del file a storage.delete()invece del percorso del campo del file. Ad esempio, sostituisci le ultime due righe di questa risposta con storage, name = photo.original_image.storage, photo.original_image.nameallora storage.delete(name).
Sean Azlin

2
@Sean +1, sto usando quella regolazione in 1.7 per eliminare le miniature generate da django-imagekit su S3 tramite django-storages. docs.djangoproject.com/en/dev/ref/files/storage/… . Nota: se stai semplicemente usando un ImageField (o FileField) puoi usare mymodel.myimagefield.delete(save=False). docs.djangoproject.com/en/dev/ref/files/file/…
user2616836

@ user2616836 Puoi usare mymodel.myimagefield.delete(save=False)su post_delete? In altre parole, posso vedere che posso eliminare il file, ma puoi eliminare il file quando viene eliminato un modello che ha il campo immagine?
eugene

1
@eugene Sì, puoi, funziona (non so perché però). In post_deletete instance.myimagefield.delete(save=False), nota l'uso di instance.
user2616836

17

Questo codice funziona bene su Django 1.4 anche con il pannello di amministrazione.

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)

È importante ottenere la memoria e il percorso prima di eliminare il modello o quest'ultimo rimarrà vuoto anche se cancellato.


3
Questo non funziona per me (Django 1.5) e il CHANGELOG di Django 1.3 afferma: "In Django 1.3, quando un modello viene eliminato, il metodo delete () di FileField non verrà chiamato. Se hai bisogno di ripulire i file orfani," Dovrò gestirlo da solo (ad esempio, con un comando di gestione personalizzato che può essere eseguito manualmente o pianificato per essere eseguito periodicamente, ad esempio tramite cron). "
darrinm

4
Questa soluzione è sbagliata! deletenon viene sempre chiamato quando una riga viene eliminata, è necessario utilizzare i segnali.
lvella

14

È necessario rimuovere il file effettivo su entrambi deletee 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)

Ottima soluzione!
AlexKh

6

Puoi prendere in considerazione l'utilizzo di un segnale pre_delete o post_delete:

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

Ovviamente, gli stessi motivi per cui è stata rimossa l'eliminazione automatica di FileField si applicano anche qui. Se elimini un file a cui si fa riferimento altrove, avrai problemi.

Nel mio caso questo sembrava appropriato perché avevo un modello di file dedicato per gestire tutti i miei file.

Nota: per qualche motivo post_delete non sembra funzionare correttamente. Il file è stato eliminato, ma il record del database è rimasto, il che è completamente l'opposto di quello che mi sarei aspettato, anche in condizioni di errore. pre_delete funziona bene però.


3
probabilmente post_deletenon funzionerà, perché file_field.delete()per impostazione predefinita salva il modello in db, prova file_field.delete(False) docs.djangoproject.com/en/1.3/ref/models/fields/…
Adam Jurczyk

3

Forse è un po 'tardi. Ma il modo più semplice per me è usare un segnale post_save. Solo per ricordare che i segnali vengono eseguiti anche durante un processo di eliminazione di QuerySet, ma il metodo [model] .delete () non viene eseguito durante il processo di eliminazione di QuerySet, quindi non è l'opzione migliore per sovrascriverlo.

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

Questa funzionalità verrà rimossa in Django 1.3, quindi non ci farei affidamento.

È possibile sostituire il deletemetodo del modello in questione per eliminare il file prima di rimuovere completamente la voce dal database.

Modificare:

Ecco un rapido esempio.

class MyModel(models.Model):

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

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

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

Hai un esempio di come usarlo in un modello per eliminare il file? Sto guardando i documenti e vedo esempi di come rimuovere l'oggetto dal database ma non vedo alcuna implementazione sulla cancellazione del file.
Narkeeso

2
Questo metodo è sbagliato perché non funzionerà per l'eliminazione collettiva (come la funzione "Elimina selezionati" dell'amministratore). Ad esempio MyModel.objects.all()[0].delete()cancellerà il file mentre MyModel.objects.all().delete()no. Usa i segnali.
Antony Hatchkins

1

Usare post_delete è sicuramente la strada giusta da percorrere. A volte le cose possono andare storte e i file non vengono eliminati. Ovviamente è possibile che tu abbia un mucchio di vecchi file che non sono stati cancellati prima che fosse usato post_delete. Ho creato una funzione che elimina i file per gli oggetti in base al fatto che il file a cui fa riferimento l'oggetto non esiste, quindi elimina l'oggetto, se il file non ha un oggetto, quindi elimina anche, inoltre può eliminare in base a un flag "attivo" per un oggetto .. Qualcosa che ho aggiunto alla maggior parte dei miei modelli. Devi passare gli oggetti che vuoi controllare, il percorso dei file degli oggetti, il campo del file e un flag per eliminare gli oggetti inattivi:

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

Ecco come puoi usarlo:

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

assicurati di scrivere " self " prima del file. quindi l'esempio sopra dovrebbe essere

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

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

Ho dimenticato il "sé" prima del mio file e non ha funzionato come stava cercando nello spazio dei nomi globale.



0

Soluzione Django 2.x:

Non è necessario installare alcun pacchetto! È molto facile da gestire in Django 2 . Ho provato la seguente soluzione utilizzando Django 2 e SFTP Storage (tuttavia penso che funzionerebbe con qualsiasi archivio)

Prima scrivi un gestore personalizzato . Quindi, se vuoi essere in grado di eliminare i file di un modello utilizzando objectsmetodi, devi scrivere e utilizzare un [Custom Manager] [3] (per sovrascrivere il delete()metodo di objects):

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

Ora devi cancellare imageprima di cancellare cancellando il modello stesso e per assegnarlo CustomManageral modello, devi siglare objectsall'interno del tuo modello:

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

Potrei avere un caso speciale poiché sto usando l'opzione upload_to sul mio campo file con nomi di directory dinamici, ma la soluzione che ho trovato è stata quella di utilizzare os.rmdir.

Nei modelli:

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
Questa è una pessima idea. Non solo rimuoverai un'intera directory rispetto a un singolo file (influendo potenzialmente su altri file), ma lo farai anche se l'eliminazione dell'oggetto effettivo non riesce.
tbm

Non è una cattiva idea se stavi lavorando sul problema che ho avuto;) Come ho già detto, ho avuto un caso d'uso unico in cui il modello da eliminare era un modello genitore. I bambini hanno scritto i file nella cartella principale e quindi se hai eliminato il genitore il comportamento desiderato era che tutti i file nella cartella fossero eliminati. Buon punto sull'ordine delle operazioni però. Non mi è venuto in mente in quel momento.
Carruthd

Preferirei comunque rimuovere i singoli file secondari quando un bambino viene eliminato; quindi, se necessario, è possibile rimuovere la directory padre quando è vuota.
tbm

Ciò ha senso mentre si estraggono oggetti figlio, ma se l'oggetto genitore viene distrutto, passare attraverso i bambini uno alla volta sembra noioso e non necessario. Indipendentemente da ciò, vedo ora che la risposta che ho dato non era abbastanza specifica per la domanda OP. Grazie per i commenti, mi hai fatto pensare di utilizzare uno strumento meno schietto in futuro.
Carruthd
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.