Django delete FileField

90

Estou construindo um aplicativo da web em Django. Tenho um modelo que carrega um arquivo, mas não consigo excluí-lo. Aqui está o meu 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)

Então, em "python manage.py shell", faço o seguinte:

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

Ele exclui do banco de dados, mas não o arquivo no servidor. O que mais posso tentar?

Obrigado!

Marcos Aguayo
fonte
Que tal usar o default_storage diretamente? docs.djangoproject.com/en/dev/topics/files
MGP

Respostas:

137

Antes do Django 1.3, o arquivo era excluído do sistema de arquivos automaticamente quando você excluía a instância de modelo correspondente. Você provavelmente está usando uma versão mais recente do Django, então terá que implementar a exclusão do arquivo do sistema de arquivos por conta própria.

Você pode fazer isso de algumas maneiras, uma das quais é usando um pre_deleteoupost_delete sinal .

Exemplo

Meu método de escolha atualmente é uma mistura de post_deleteepre_save sinais, o que faz com que arquivos obsoletos sejam excluídos sempre que os modelos correspondentes forem excluídos ou seus arquivos alterados.

Com base em um 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 extremo: se o seu aplicativo carrega um novo arquivo e aponta a instância do modelo para o novo arquivo sem chamar save()(por exemplo, atualizando em massa a QuerySet), o arquivo antigo continuará parado porque os sinais não serão executados. Isso não acontece se você usar métodos convencionais de tratamento de arquivos.
  • Acho que um dos aplicativos que construí tem esse código em produção, mas use por sua própria conta e risco.
  • Estilo de codificação: este exemplo usa filecomo nome de campo, o que não é um bom estilo porque entra em conflito com ofile identificador de objeto .

Veja também

  • FieldFile.delete()na referência de campo do modelo Django 1.11 (observe que ele descreve a FieldFileclasse, mas você chamaria .delete()diretamente no campo: FileFieldproxies de instância para o correspondenteFieldFile instância , e você acessa seus métodos como se fossem de campo)

    Observe que quando um modelo é excluído, os arquivos relacionados não são excluídos. Se você precisa limpar arquivos órfãos, você mesmo precisa lidar com isso (por exemplo, com um comando de gerenciamento personalizado que pode ser executado manualmente ou programado para ser executado periodicamente via, por exemplo, cron).

  • Por que o Django não deleta arquivos automaticamente: entrada nas notas de lançamento do Django 1.3

    Em versões anteriores do Django, quando uma instância de modelo contendo um FileFieldera excluída, FileFieldassumia a responsabilidade de também excluir o arquivo do armazenamento de backend. Isso abriu a porta para vários cenários de perda de dados, incluindo transações revertidas e campos em diferentes modelos que fazem referência ao mesmo arquivo. No Django 1.3, quando um modelo é excluído da FileField's delete()método não será chamado. Se você precisa de limpeza de arquivos órfãos, você precisa lidar com isso sozinho (por exemplo, com um comando de gerenciamento personalizado que pode ser executado manualmente ou programado para ser executado periodicamente via, por exemplo, cron).

  • Exemplo de uso de pre_deleteapenas um sinal

Anton Strogonoff
fonte
2
Sim, mas certifique-se de fazer as verificações apropriadas. (Espere um pouco, postarei o código que encontrei em uso no sistema real.)
Anton Strogonoff
7
Provavelmente é melhor usar instance.song.delete(save=False), já que usa o mecanismo de armazenamento django correto.
Eduardo
1
Raro hoje em dia que copio código que não seria capaz de escrever diretamente do SO e funciona com modificações limitadas. Ajuda fantástica, obrigado!
GJStein
Encontrado um bug neste onde se a instância existe, mas nenhuma imagem foi salva anteriormente, então os.path.isfile(old_file.path)falha porque old_file.pathlevanta um erro (nenhum arquivo está associado ao campo). Consertei adicionando if old_file:pouco antes da chamada para os.path.isfile().
three_pineapples
@three_pineapples faz sentido. Pode ser que a restrição NOT NULL no campo do arquivo tenha sido ignorada ou não tenha sido encerrada em algum ponto, caso em que alguns objetos a teriam vazio.
Anton Strogonoff
73

Tente django-cleanup , ele invoca automaticamente o método delete em FileField quando você remove o modelo.

pip install django-cleanup

settings.py

INSTALLED_APPS = (
     ...
    'django_cleanup', # should go after your apps
)
un1t
fonte
Legal, ele precisa ser adicionado ao FileField por padrão, obrigado!
megajoe
Ele também está excluindo o arquivo durante o upload
chirag soni
Uau. Eu estava tentando fazer com que isso não acontecesse e não conseguia descobrir o porquê. Alguém o instalou anos atrás e se esqueceu dele. Obrigado.
ryan28561
3
Então, por que o Django removeu a função de exclusão de filefield em primeiro lugar?
ha-neul
Você é a lenda !!
marlonjd
28

Você pode excluir o arquivo do sistema de arquivos com o .deletemétodo de chamada do campo de arquivo mostrado abaixo com Django> = 1.10:

obj = Song.objects.get(pk=1)
obj.song.delete()
Mesut Tasci
fonte
4
Deve ser a resposta aceita, simples e funciona bem.
Nikolay Shindarov
13

Você também pode simplesmente sobrescrever a função de exclusão do modelo para verificar se o arquivo existe e excluí-lo antes de chamar a função super.

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
fonte
8
Esteja ciente de que a chamada queryset.delete()não limpará os arquivos com esta solução. Você precisaria iterar sobre o queryset e chamar .delete()cada objeto.
Scott Woodall
Eu sou novo no Django. Isso é bom, mas e se o modelo estivesse herdando de uma classe abstrata que substituiu o método de exclusão, isso não substituiria o da classe abstrata? Usar sinais parece melhor para mim
theTypan
8

Solução Django 2.x:

É muito fácil lidar com a exclusão de arquivos no Django 2 . Eu tentei seguir a solução usando Django 2 e SFTP Storage e também FTP STORAGE, e tenho certeza de que funcionará com qualquer outro gerenciador de armazenamento que implementou o deletemétodo. ( deletemétodo é um dosstorage métodos abstratos).

Substitua o deletemétodo do modelo de forma que a instância exclua seus FileFields antes de excluir a si mesma:

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 muito fácil para mim. Se você quiser verificar se o arquivo existe antes da exclusão, você pode usar storage.exists. por exemplo self.song.storage.exists(self.song.name), retornará um booleanrepresentando se a música existe. Então, ficará assim:

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 (além):

Como @HeyMan mencionou, com esta solução, chamar Song.objects.all().delete()não exclui arquivos! Isso está acontecendo porque Song.objects.all().delete()está executando a consulta de exclusão do Default Manager . Portanto, se você deseja excluir arquivos de um modelo usando objectsmétodos, deve escrever e usar um gerenciador personalizado (apenas para substituir sua consulta de exclusão):

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

e para atribuir o CustomManagerao modelo, você deve rubricar objectsem seu 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()

Agora você pode usar .delete()no final de quaisquer objectssubconsultas. Eu escrevi o mais simples CustomManager, mas você pode fazer melhor retornando algo sobre os objetos que você excluiu ou o que quiser.

Hamidreza
fonte
1
Sim, acho que eles adicionaram esse recurso desde que postei a pergunta.
Marcos Aguayo
1
Mesmo assim, delete não é chamado de wenn chamando Song.objects.all (). Delete (). Mesmo para quando a instância é excluída por on_delete = models.CASCADE.
HeyMan
@HeyMan eu resolvi e editei minha solução agora :)
Hamidreza
4

Este é um aplicativo que removerá arquivos antigos sempre que o modelo for excluído ou um novo arquivo for carregado: 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
fonte
3

@Anton Strogonoff

Estou faltando alguma coisa no código quando um arquivo muda, se criar um novo arquivo gera um erro, pois é um arquivo novo e não encontrou um caminho. Modifiquei o código de função e adicionei uma frase try / except e funciona bem.

@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
desenvolvedor de Java
fonte
Eu não encontrei isso - pode ser um bug no meu código ou algo mudou no Django. Eu sugeriria capturar uma exceção específica em seu try:bloco ( AttributeErrortalvez?).
Anton Strogonoff
Não é uma ideia tão boa usar a biblioteca OS, pois você encontrará problemas se migrar para outro armazenamento (Amazon S3, por exemplo).
Igor Pomaranskiy
@IgorPomaranskiy o que aconteceria em um armazenamento como o Amazon S3 quando você usa os.remove ??
Daniel González Fernández
@ DanielGonzálezFernández Acho que vai falhar (com um erro como algo sobre um caminho não existente). É por isso que o Django usa abstrações para armazenamentos.
Igor Pomaranskiy
0

Este código será executado toda vez que eu carregar uma nova imagem (campo de logotipo) e verificar se já existe um logotipo, feche-o e remova-o do disco. O mesmo procedimento pode ser feito na função de receptor. Espero que isto ajude.

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