Django löschen FileField

90

Ich baue eine Web-App in Django. Ich habe ein Modell, das eine Datei hochlädt, aber ich kann sie nicht löschen. Hier ist mein Code:

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)

Dann mache ich in "python manage.py shell" Folgendes:

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

Es wird aus der Datenbank gelöscht, jedoch nicht die Datei auf dem Server. Was kann ich noch versuchen?

Vielen Dank!

Marcos Aguayo
quelle
Was ist mit der direkten Verwendung von default_storage? docs.djangoproject.com/de/dev/topics/files
MGP

Antworten:

137

Vor Django 1.3 wurde die Datei automatisch aus dem Dateisystem gelöscht, wenn Sie die entsprechende Modellinstanz gelöscht haben. Sie verwenden wahrscheinlich eine neuere Django-Version, daher müssen Sie das Löschen der Datei aus dem Dateisystem selbst implementieren.

Sie können dies auf verschiedene Arten tun, von denen eine ein pre_deleteoder ein post_deleteSignal verwendet.

Beispiel

Meine bevorzugte Methode ist derzeit eine Mischung aus post_deleteund pre_saveSignalen, wodurch veraltete Dateien gelöscht werden, wenn entsprechende Modelle gelöscht oder ihre Dateien geändert werden.

Basierend auf einem hypothetischen MediaFileModell:

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)
  • Randfall: Wenn Ihre App eine neue Datei hochlädt und die Modellinstanz auf die neue Datei verweist, ohne sie aufzurufen save()(z. B. durch Massenaktualisierung von a QuerySet), liegt die alte Datei weiterhin herum, da keine Signale ausgeführt werden. Dies ist nicht der Fall, wenn Sie herkömmliche Methoden zur Dateiverwaltung verwenden.
  • Ich denke, eine der Apps, die ich erstellt habe, hat diesen Code in der Produktion, verwendet ihn jedoch auf eigenes Risiko.
  • Codierungsstil: In diesem Beispiel wird fileals Feldname verwendet. Dies ist kein guter Stil, da er mit der integrierten fileObjektkennung kollidiert .

Siehe auch

  • FieldFile.delete()in Django 1.11 Modellfeldreferenz (beachten Sie, dass es die FieldFileKlasse beschreibt , aber Sie würden .delete()direkt das Feld aufrufen : FileFieldInstanz-Proxys für die entsprechende FieldFileInstanz, und Sie greifen auf ihre Methoden zu, als wären sie Felder)

    Beachten Sie, dass beim Löschen eines Modells verwandte Dateien nicht gelöscht werden. Wenn Sie verwaiste Dateien bereinigen müssen, müssen Sie dies selbst tun (z. B. mit einem benutzerdefinierten Verwaltungsbefehl, der manuell ausgeführt oder so geplant werden kann, dass er regelmäßig über z. B. cron ausgeführt wird).

  • Warum Django Dateien nicht automatisch löscht: Eintrag in Versionshinweisen für Django 1.3

    In früheren Django-Versionen, als eine Modellinstanz mit a FileFieldgelöscht wurde, musste FileFielddie Datei auch aus dem Backend-Speicher gelöscht werden. Dies öffnete die Tür zu mehreren Datenverlustszenarien, einschließlich Rollback-Transaktionen und Feldern in verschiedenen Modellen, die auf dieselbe Datei verweisen. In Django 1.3 wird die Methode von FileField' delete()nicht aufgerufen , wenn ein Modell gelöscht wird. Wenn Sie verwaiste Dateien bereinigen müssen, müssen Sie dies selbst tun (z. B. mit einem benutzerdefinierten Verwaltungsbefehl, der manuell ausgeführt oder so geplant werden kann, dass er regelmäßig über z. B. cron ausgeführt wird).

  • Beispiel für die Verwendung nur eines pre_deleteSignals

Anton Strogonoff
quelle
2
Ja, aber stellen Sie sicher, dass Sie die entsprechenden Überprüfungen durchführen. (Geben Sie mir eine Sekunde, ich werde den Code veröffentlichen, den ich im tatsächlichen System gefunden habe.)
Anton Strogonoff
7
Es ist wahrscheinlich besser zu verwenden instance.song.delete(save=False), da es die richtige Django-Speicher-Engine verwendet.
Eduardo
1
Selten heutzutage, dass ich Code kopiere, hätte ich mich nicht direkt aus SO schreiben können und es funktioniert mit begrenzten Modifikationen. Fantastische Hilfe, danke!
GJStein
Es wurde ein Fehler gefunden, bei dem, wenn die Instanz vorhanden ist, aber zuvor kein Bild gespeichert wurde, dies os.path.isfile(old_file.path)fehlschlägt, weil old_file.pathein Fehler ausgelöst wird (dem Feld ist keine Datei zugeordnet). Ich habe es behoben, indem ich if old_file:kurz vor dem Anruf zu hinzugefügt habe os.path.isfile().
Three_Pineapples
@three_pineapples macht Sinn. Es kann sein, dass die NOT NULL-Einschränkung für das Dateifeld umgangen wurde oder irgendwann nicht beendet wurde. In diesem Fall wäre sie bei einigen Objekten leer.
Anton Strogonoff
73

Versuchen Sie es mit django-cleanup. Beim Entfernen des Modells wird automatisch die Löschmethode in FileField aufgerufen.

pip install django-cleanup

settings.py

INSTALLED_APPS = (
     ...
    'django_cleanup', # should go after your apps
)
un1t
quelle
Cool, es muss standardmäßig zu FileField hinzugefügt werden, danke!
Megajoe
Es löscht die Datei auch beim Hochladen
chirag soni
Beeindruckend. Ich habe versucht, dies nicht zu erreichen, und ich konnte nicht herausfinden, warum es so war. Jemand hatte dies vor Jahren installiert und vergessen. Vielen Dank.
Ryan28561
2
Warum hat Django die Funktion zum Löschen von Dateifeldern überhaupt entfernt?
ha-neul
Du bist die Legende !!
Marlonjd
28

Sie können die Datei aus dem Dateisystem löschen .delete, indem Sie die unten gezeigte Aufrufmethode des Dateifelds mit Django> = 1.10 aufrufen :

obj = Song.objects.get(pk=1)
obj.song.delete()
Mesut Tasci
quelle
4
Sollte die akzeptierte Antwort sein, einfach und funktioniert einfach.
Nikolay Shindarov
13

Sie können die Löschfunktion des Modells auch einfach überschreiben, um zu prüfen, ob eine Datei vorhanden ist, und diese löschen, bevor Sie die Superfunktion aufrufen.

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
quelle
8
Beachten Sie, dass beim Aufrufen queryset.delete()die Dateien mit dieser Lösung nicht bereinigt werden. Sie müssten das Abfrageset durchlaufen und .delete()jedes Objekt aufrufen .
Scott Woodall
Ich bin neu in Django. Das ist gut, aber was wäre, wenn das Modell von einer abstrakten Klasse erben würde, die die Löschmethode überschrieben hat, würde dies nicht die von der abstrakten Klasse überschreiben? Die Verwendung von Signalen erscheint mir besser
theTypan
8

Django 2.x Lösung:

In Django 2 ist das Löschen von Dateien sehr einfach . Ich habe versucht, die folgende Lösung mit Django 2 und SFTP Storage sowie FTP STORAGE zu verwenden, und ich bin mir ziemlich sicher, dass sie mit allen anderen Speichermanagern funktioniert, die die deleteMethode implementiert haben . ( deleteMethode ist eine der storageabstrakten Methoden.)

Überschreiben Sie die deleteMethode des Modells so, dass die Instanz ihre FileFields löscht, bevor sie sich selbst löscht:

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

Es funktioniert ziemlich einfach für mich. Wenn Sie vor dem Löschen überprüfen möchten, ob eine Datei vorhanden ist, können Sie diese verwenden storage.exists. zB self.song.storage.exists(self.song.name)gibt eine booleanDarstellung zurück, wenn das Lied existiert. So wird es aussehen:

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 (zusätzlich):

Wie @HeyMan erwähnt, werden mit dieser Lösung beim Aufrufen Song.objects.all().delete()keine Dateien gelöscht! Dies geschieht, weil Song.objects.all().delete()die Löschabfrage von Default Manager ausgeführt wird . Wenn Sie also Dateien eines Modells mithilfe von objectsMethoden löschen möchten , müssen Sie einen benutzerdefinierten Manager schreiben und verwenden (nur um die Löschabfrage zu überschreiben):

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

und um das CustomManagerdem Modell zuzuweisen, müssen Sie objectsin Ihrem Modell initialisieren:

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

Jetzt können Sie .delete()am Ende alle objectsUnterabfragen verwenden. Ich habe das einfachste geschrieben CustomManager, aber Sie können es besser machen, indem Sie etwas über gelöschte Objekte oder was auch immer Sie wollen zurückgeben.

Hamidreza
quelle
1
Ja, ich denke, sie haben diese Funktion hinzugefügt, seit ich die Frage gestellt habe.
Marcos Aguayo
1
Das Löschen wird immer noch nicht aufgerufen, wenn Song.objects.all (). Delete () aufgerufen wird. Gleiches gilt, wenn die Instanz von on_delete = models.CASCADE gelöscht wird.
HeyMan
@ HeyMan Ich habe es gelöst und meine Lösung gerade bearbeitet :)
Hamidreza
4

Hier ist eine App, die alte Dateien entfernt, wenn ein Modell gelöscht oder eine neue Datei hochgeladen wird: 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
quelle
3

@ Anton Strogonoff

Ich vermisse etwas im Code, wenn eine Datei geändert wird. Wenn Sie eine neue Datei erstellen, wird ein Fehler generiert, da es sich um eine neue Datei handelt, die keinen Pfad gefunden hat. Ich habe den Funktionscode geändert und einen try / without-Satz hinzugefügt, der gut funktioniert.

@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
Java Entwickler
quelle
Ich bin darauf nicht gestoßen - könnte ein Fehler in meinem Code sein oder etwas, das sich in Django geändert hat. Ich würde jedoch vorschlagen, eine bestimmte Ausnahme in Ihrem try:Block abzufangen ( AttributeErrorvielleicht?).
Anton Strogonoff
Es ist keine so gute Idee, die OS-Bibliothek zu verwenden, da bei der Migration auf einen anderen Speicher (z. B. Amazon S3) Probleme auftreten.
Igor Pomaranskiy
@IgorPomaranskiy was würde in einem Speicher wie Amazon S3 passieren, wenn Sie os.remove verwenden?
Daniel González Fernández
@ DanielGonzálezFernández Ich denke, es wird fehlschlagen (mit einem Fehler wie etwas über nicht existierenden Pfad). Deshalb verwendet Django Abstraktionen für Speicher.
Igor Pomaranskiy
OK danke!!
Daniel González Fernández
0

Dieser Code wird jedes Mal ausgeführt, wenn ich ein neues Bild (Logo-Feld) hochlade und prüfe, ob bereits ein Logo vorhanden ist. Schließen Sie es und entfernen Sie es von der Festplatte. Das gleiche Verfahren könnte natürlich in der Empfängerfunktion durchgeführt werden. Hoffe das hilft.

 #  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
quelle