Django elimina FileField


93

Sto creando un'app Web in Django. Ho un modello che carica un file, ma non riesco a eliminarlo. Ecco il mio codice:

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)

Quindi, in "python manage.py shell" faccio questo:

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

Elimina dal database ma non il file sul server. Cos'altro posso provare?

Grazie!


Che dire dell'utilizzo diretto di default_storage? docs.djangoproject.com/en/dev/topics/files
MGP

Risposte:


141

Prima di Django 1.3, il file veniva eliminato automaticamente dal filesystem quando si eliminava l'istanza del modello corrispondente. Probabilmente stai usando una versione più recente di Django, quindi dovrai implementare l'eliminazione del file dal filesystem da solo.

Puoi farlo in diversi modi, uno dei quali utilizza un segnale pre_deleteo post_delete.

Esempio

Il mio metodo di scelta attualmente è un mix di segnali post_deletee pre_save, il che fa in modo che i file obsoleti vengano eliminati ogni volta che i modelli corrispondenti vengono eliminati o vengono modificati i loro file.

Basato su un MediaFilemodello ipotetico :

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)
  • Edge case: se la tua app carica un nuovo file e punta l'istanza del modello sul nuovo file senza chiamare save()(ad es. Aggiornando in blocco a QuerySet), il vecchio file continuerà a rimanere nascosto perché i segnali non verranno eseguiti. Ciò non accade se si utilizzano metodi di gestione dei file convenzionali.
  • Penso che una delle app che ho creato abbia questo codice in produzione, ma lo usi comunque a tuo rischio.
  • Stile di codifica: questo esempio utilizza filecome nome di campo, che non è un buon stile perché contrasta con l' fileidentificatore di oggetto incorporato .

Guarda anche

  • FieldFile.delete()nel riferimento al campo del modello Django 1.11 (nota che descrive la FieldFileclasse, ma chiameresti .delete()direttamente sul campo: FileFieldproxy di FieldFileistanza all'istanza corrispondente e accedi ai suoi metodi come se fossero di campo)

    Notare che quando un modello viene eliminato, i file correlati non vengono eliminati. Se hai bisogno di ripulire file orfani, dovrai gestirlo da solo (ad esempio, con un comando di gestione personalizzato che può essere eseguito manualmente o pianificato per essere eseguito periodicamente tramite ad esempio cron).

  • Perché Django non elimina automaticamente i file: voce nelle note di rilascio per Django 1.3

    Nelle versioni precedenti di Django, quando un'istanza del modello contenente a FileFieldveniva eliminata, FileFieldsi impegnava a eliminare anche il file dall'archivio back-end. Ciò ha aperto la porta a diversi scenari di perdita di dati, comprese transazioni di rollback e campi su modelli diversi che fanno riferimento allo stesso file. In Django 1.3, quando un modello viene eliminato, il metodo FileFields delete()non verrà chiamato. Se hai bisogno di ripulire i file orfani, dovrai gestirlo da solo (ad esempio, con un comando di gestione personalizzato che può essere eseguito manualmente o pianificato per essere eseguito periodicamente tramite ad esempio cron).

  • Esempio di utilizzo di un pre_deletesolo segnale


2
Sì, ma assicurati di eseguire i controlli appropriati. (Dammi un secondo, inserirò il codice che ho trovato in uso nel sistema attuale.)
Anton Strogonoff

7
Probabilmente è meglio da usare instance.song.delete(save=False), poiché utilizza il corretto motore di archiviazione django.
Eduardo

1
Raramente oggigiorno che copio codice non sarei stato in grado di scrivermi direttamente da SO e funziona con modifiche limitate. Aiuto fantastico, grazie!
GJStein

Trovato un bug in questo dove se l'istanza esiste, ma nessuna immagine è stata salvata in precedenza, os.path.isfile(old_file.path)fallisce perché old_file.pathgenera un errore (nessun file è associato al campo). L'ho risolto aggiungendo if old_file:appena prima della chiamata a os.path.isfile().
three_pineapples

@three_pineapples ha senso. Potrebbe essere che il vincolo NOT NULL sul campo file sia stato aggirato o non sia uscito a un certo punto, nel qual caso alcuni oggetti lo avrebbero vuoto.
Anton Strogonoff

78

Prova django-cleanup , richiama automaticamente il metodo di eliminazione su FileField quando rimuovi il modello.

pip install django-cleanup

settings.py

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

Fantastico, deve essere aggiunto a FileField per impostazione predefinita, grazie!
megajoe

Sta eliminando il file anche durante il caricamento
chirag soni

Wow. Stavo cercando di evitare che ciò accadesse e non riuscivo a capire perché fosse. Qualcuno l'aveva installato anni prima e se ne era dimenticato. Grazie.
ryan28561

4
Allora, perché Django ha rimosso la funzione di cancellazione del filefield in primo luogo?
ha-neul

Sei la leggenda !!
marlonjd

32

Puoi eliminare il file dal filesystem chiamando il .deletemetodo del campo file mostrato come sotto con Django> = 1.10:

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

7
Dovrebbe essere la risposta accettata, semplice e giusta.
Nikolay Shindarov,

14

Puoi anche sovrascrivere semplicemente la funzione di cancellazione del modello per verificare se il file esiste ed eliminarlo prima di chiamare la super funzione.

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)

8
Fai attenzione che la chiamata queryset.delete()non ripulirà i file con questa soluzione. Dovresti iterare sul set di query e chiamare .delete()su ogni oggetto.
Scott Woodall

Sono nuovo su Django. Questo è positivo, ma cosa succede se il modello eredita da una classe astratta che ha sovrascritto il metodo delete, questo non sovrascriverà quello dalla classe astratta? Usare i segnali mi sembra migliore
theTypan

8

Soluzione Django 2.x:

È molto facile gestire l'eliminazione dei file in Django 2 . Ho provato la seguente soluzione utilizzando Django 2 e SFTP Storage e anche FTP STORAGE, e sono abbastanza sicuro che funzionerà con qualsiasi altro gestore di archiviazione che abbia implementato il deletemetodo. (il deletemetodo è uno dei storagemetodi astratti.)

Sostituisci il deletemetodo del modello in modo che l'istanza elimini i propri FileField prima di eliminarsi:

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

Funziona abbastanza facilmente per me. Se vuoi controllare se il file esiste prima dell'eliminazione, puoi usare storage.exists. es. self.song.storage.exists(self.song.name)restituirà una booleanrappresentazione se la canzone esiste. Quindi sarà simile a questo:

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

EDIT (in aggiunta):

Come menzionato da @HeyMan , con questa soluzione la chiamata Song.objects.all().delete()non cancella i file! Ciò sta accadendo perché Song.objects.all().delete()sta eseguendo la query di eliminazione di Default Manager . Quindi, se vuoi essere in grado di eliminare i file di un modello utilizzando objectsmetodi, devi scrivere e utilizzare un gestore personalizzato (solo per sovrascrivere la sua query di eliminazione):

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

e per assegnare il CustomManageral modello, devi siglare objectsall'interno del tuo modello:

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

Ora puoi usare .delete()alla fine di qualsiasi objectssottoquery. Ho scritto il più semplice CustomManager, ma puoi farlo meglio restituendo qualcosa sugli oggetti che hai cancellato o quello che vuoi.


1
Sì, penso che abbiano aggiunto quella funzione da quando ho pubblicato la domanda.
Marcos Aguayo

1
Still delete non viene chiamato quando si chiama Song.objects.all (). Delete (). Lo stesso per quando l'istanza viene eliminata da on_delete = models.CASCADE.
HeyMan

@HeyMan L'ho risolto e ho modificato la mia soluzione adesso :)
Hamidreza

4

Ecco un'app che rimuoverà i vecchi file ogni volta che il modello viene eliminato o viene caricato un nuovo file: 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)

3

@Anton Strogonoff

Mi manca qualcosa nel codice quando un file cambia, se crei un nuovo file genera un errore, perché è un nuovo file a non ha trovato un percorso. Ho modificato il codice della funzione e aggiunto una frase try / tranne e funziona bene.

@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

Non l'ho riscontrato, potrebbe essere un bug nel mio codice o qualcosa di cambiato in Django. Suggerirei di catturare un'eccezione specifica nel tuo try:blocco, però ( AttributeErrorforse?).
Anton Strogonoff

Non è una buona idea utilizzare la libreria del sistema operativo, poiché incontrerai problemi se esegui la migrazione a un archivio diverso (Amazon S3, ad esempio).
Igor Pomaranskiy

@IgorPomaranskiy cosa succederebbe in uno storage come Amazon S3 quando usi os.remove ??
Daniel González Fernández

@ DanielGonzálezFernández Immagino che fallirà (con un errore come qualcosa su un percorso inesistente). Ecco perché Django usa le astrazioni per gli archivi.
Igor Pomaranskiy il

0

Questo codice verrà eseguito ogni volta che caricherò una nuova immagine (campo del logo) e verificherò se esiste già un logo, in tal caso chiudilo e rimuovilo dal disco. La stessa procedura potrebbe ovviamente essere eseguita nella funzione ricevitore. Spero che sia di aiuto.

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