Django supprimer FileField

93

Je crée une application Web dans Django. J'ai un modèle qui télécharge un fichier, mais je ne peux pas le supprimer. Voici mon 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)

Ensuite, dans "python manage.py shell" je fais ceci:

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

Il supprime de la base de données mais pas le fichier sur le serveur. Que puis-je essayer d'autre?

Merci!

Marcos Aguayo
la source
Qu'en est-il de l'utilisation directe de default_storage? docs.djangoproject.com/en/dev/topics/files
MGP

Réponses:

142

Avant Django 1.3, le fichier était automatiquement supprimé du système de fichiers lorsque vous supprimiez l'instance de modèle correspondante. Vous utilisez probablement une version plus récente de Django, vous devrez donc implémenter la suppression du fichier du système de fichiers vous-même.

Vous pouvez le faire de plusieurs manières, l'une d'elles utilisant un pre_deleteoupost_delete signal .

Exemple

Ma méthode de choix est actuellement un mélange de post_deleteetpre_save signaux, ce qui permet de supprimer les fichiers obsolètes chaque fois que les modèles correspondants sont supprimés ou que leurs fichiers sont modifiés.

Basé sur un MediaFilemodèle hypothétique :

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)
  • Cas Edge: si votre application télécharge un nouveau fichier et pointe l'instance de modèle vers le nouveau fichier sans appeler save()(par exemple en mettant à jour en masse a QuerySet), l'ancien fichier continuera à traîner car les signaux ne seront pas exécutés. Cela ne se produit pas si vous utilisez des méthodes de gestion de fichiers conventionnelles.
  • Je pense que l'une des applications que j'ai construites a ce code en production, mais l'utilise néanmoins à vos risques et périls.
  • Style de codage: cet exemple utilise filecomme nom de champ, ce qui n'est pas un bon style car il entre en conflit avec l' fileidentifiant d'objet intégré .

Voir également

  • FieldFile.delete()dans la référence de champ de modèle Django 1.11 (notez qu'elle décrit la FieldFileclasse, mais vous appelleriez .delete()directement sur le champ: FileFieldinstance proxies vers l' FieldFileinstance correspondante , et vous accédez à ses méthodes comme s'il s'agissait de champs)

    Notez que lorsqu'un modèle est supprimé, les fichiers associés ne sont pas supprimés. Si vous avez besoin de nettoyer des fichiers orphelins, vous devrez le gérer vous-même (par exemple, avec une commande de gestion personnalisée qui peut être exécutée manuellement ou planifiée pour s'exécuter périodiquement via, par exemple, cron).

  • Pourquoi Django ne supprime pas automatiquement les fichiers: entrée dans les notes de publication de Django 1.3

    Dans les versions antérieures de Django, lorsqu'une instance de modèle contenant un FileFieldétait supprimée, FileFieldil prenait sur elle de supprimer également le fichier du stockage backend. Cela a ouvert la porte à plusieurs scénarios de perte de données, y compris des transactions annulées et des champs sur différents modèles référençant le même fichier. Dans Django 1.3, lorsqu'un modèle est supprimé, la méthode FileFields delete()ne sera pas appelée. Si vous avez besoin de nettoyer des fichiers orphelins, vous devrez le gérer vous-même (par exemple, avec une commande de gestion personnalisée qui peut être exécutée manuellement ou planifiée pour s'exécuter périodiquement via, par exemple, cron).

  • Exemple d'utilisation d'un pre_deletesignal uniquement

Anton Strogonoff
la source
2
Oui, mais assurez-vous de faire les vérifications appropriées. (Donnez-moi une seconde, je posterai le code que j'ai trouvé en cours d'utilisation dans le système actuel.)
Anton Strogonoff
7
Il est probablement préférable d'utiliser instance.song.delete(save=False), car il utilise le bon moteur de stockage django.
Eduardo
1
Rare de nos jours que je copie du code que je n'aurais pas pu m'écrire directement depuis SO et cela fonctionne avec des modifications limitées. Une aide fantastique, merci!
GJStein
Un bogue a été détecté à cet endroit où si l'instance existe, mais qu'aucune image n'a été précédemment enregistrée, os.path.isfile(old_file.path)échoue car elle old_file.pathgénère une erreur (aucun fichier n'est associé au champ). Je l'ai corrigé en ajoutant if old_file:juste avant l'appel à os.path.isfile().
three_pineapples
@three_pineapples a du sens. Il se peut que la contrainte NOT NULL sur le champ de fichier ait été contournée ou ne se soit pas terminée à un moment donné, auquel cas certains objets l'auraient vide.
Anton Strogonoff
78

Essayez django-cleanup , il invoque automatiquement la méthode de suppression sur FileField lorsque vous supprimez le modèle.

pip install django-cleanup

settings.py

INSTALLED_APPS = (
     ...
    'django_cleanup', # should go after your apps
)
un1t
la source
Cool, il doit être ajouté à FileField par défaut, merci!
megajoe
Il supprime également le fichier lors du téléchargement
chirag soni
Sensationnel. J'essayais de faire en sorte que cela ne se produise pas et je ne pouvais pas comprendre pourquoi. Quelqu'un l'avait installé il y a des années et l'avait oublié. Merci.
ryan28561
4
Alors, pourquoi Django a supprimé la fonction de suppression de champ de fichiers en premier lieu?
ha-neul
Vous êtes la légende !!
marlonjd le
32

Vous pouvez supprimer le fichier du système de fichiers avec la .deleteméthode d' appel du champ de fichier ci-dessous avec Django> = 1.10:

obj = Song.objects.get(pk=1)
obj.song.delete()
Mesut Tasci
la source
7
Devrait être la réponse acceptée, simple et fonctionne juste.
Nikolay Shindarov le
14

Vous pouvez également simplement écraser la fonction de suppression du modèle pour vérifier si le fichier existe et le supprimer avant d'appeler la super fonction.

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
la source
8
Attention, l'appel queryset.delete()ne nettoiera pas les fichiers avec cette solution. Vous devrez parcourir l'ensemble de requêtes et appeler .delete()chaque objet.
Scott Woodall
Je suis nouveau sur Django. C'est bien, mais que se passe-t-il si le modèle hérite d'une classe abstraite qui a remplacé la méthode de suppression, cela ne remplacerait-il pas celui de la classe abstraite? Utiliser les signaux me paraît mieux
theTypan
8

Solution Django 2.x:

Il est très facile de gérer la suppression de fichiers dans Django 2 . J'ai essayé la solution suivante en utilisant Django 2 et le stockage SFTP ainsi que le STOCKAGE FTP, et je suis à peu près sûr que cela fonctionnera avec tous les autres gestionnaires de stockage qui ont implémenté la deleteméthode. (la deleteméthode est l'une des storageméthodes abstraites.)

Remplacez la deleteméthode du modèle de manière à ce que l'instance supprime ses FileFields avant de se supprimer:

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

Cela fonctionne assez facilement pour moi. Si vous souhaitez vérifier si le fichier existe avant la suppression, vous pouvez utiliser storage.exists. par exemple self.song.storage.exists(self.song.name)retournera un booleanreprésentant si la chanson existe. Cela ressemblera donc à ceci:

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 (en plus):

Comme @HeyMan l'a mentionné, avec cette solution, l'appel Song.objects.all().delete()ne supprime pas les fichiers! Cela se produit car Song.objects.all().delete()exécute la requête de suppression de Default Manager . Donc, si vous voulez pouvoir supprimer les fichiers d'un modèle à l'aide de objectsméthodes, vous devez écrire et utiliser un gestionnaire personnalisé (juste pour remplacer sa requête de suppression):

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

et pour affecter le CustomManagerau modèle, vous devez initialiser objectsà l' intérieur de votre modèle:

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

Vous pouvez maintenant utiliser .delete()à la fin de toutes les objectssous-requêtes. J'ai écrit le plus simple CustomManager, mais vous pouvez le faire mieux en renvoyant quelque chose sur les objets que vous avez supprimés ou ce que vous voulez.

Hamidreza
la source
1
Ouais, je pense qu'ils ont ajouté cette fonctionnalité depuis que j'ai posté la question.
Marcos Aguayo
1
La suppression continue n'est pas appelée si vous appelez Song.objects.all (). Delete (). Idem lorsque l'instance est supprimée par on_delete = models.CASCADE.
HeyMan
@HeyMan Je l'ai résolu et modifié ma solution maintenant :)
Hamidreza
4

Voici une application qui supprimera les anciens fichiers chaque fois qu'un modèle est supprimé ou qu'un nouveau fichier est téléchargé: 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
la source
3

@Anton Strogonoff

Il me manque quelque chose dans le code lors d'un changement de fichier, si vous créez un nouveau fichier génère une erreur, car il s'agit d'un nouveau fichier qui n'a pas trouvé de chemin. J'ai modifié le code de la fonction et ajouté une phrase try / except et cela fonctionne 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
développeur Java
la source
Je n'ai pas rencontré cela - cela pourrait être un bogue dans mon code ou quelque chose de changé dans Django. Je suggérerais d'attraper une exception spécifique dans votre try:bloc, cependant ( AttributeErrorpeut-être?).
Anton Strogonoff
Ce n'est pas une bonne idée d'utiliser la bibliothèque os, car vous rencontrerez des problèmes si vous migrez vers un stockage différent (Amazon S3, par exemple).
Igor Pomaranskiy
@IgorPomaranskiy que se passerait-il dans un stockage comme Amazon S3 lorsque vous utilisez os.remove ??
Daniel González Fernández
@ DanielGonzálezFernández Je suppose que cela échouera (avec une erreur comme quelque chose à propos d'un chemin inexistant). C'est pourquoi Django utilise des abstractions pour les stockages.
Igor Pomaranskiy le
0

Ce code s'exécutera chaque fois que je télécharge une nouvelle image (champ de logo) et vérifie si un logo existe déjà, fermez-le et supprimez-le du disque. La même procédure pourrait bien sûr être effectuée en fonction du récepteur. J'espère que cela t'aides.

 #  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
la source