Como faço para que o Django Admin exclua arquivos quando removo um objeto do banco de dados / modelo?

86

Estou usando 1.2.5 com um ImageField padrão e usando o back-end de armazenamento embutido. Os arquivos são carregados sem problemas, mas quando removo uma entrada do administrador, o arquivo real no servidor não é excluído.

Narkeeso
fonte
Hm, na verdade deveria. Verifique as permissões do arquivo em sua pasta de upload (mude para 0777).
Torsten Engelbrecht,
5
Django removeu o recurso de exclusão automática (para Googlers que viram o comentário acima).
Marque em

Respostas:

102

Você pode receber o sinal pre_deleteou post_delete(veja o comentário de @toto_tico abaixo) e chamar o método delete () no objeto FileField, assim (em 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)
darrinm
fonte
11
Certifique-se de adicionar uma verificação se o instance.filecampo não está vazio ou pode (pelo menos tentar) excluir todo o diretório MEDIA_ROOT. Isso se aplica até mesmo aos ImageField(null=False)campos.
Antony Hatchkins de
49
Obrigado. Em geral, eu recomendaria usar o post_deletesinal porque é mais seguro caso o delete falhe por algum motivo. Então, nem o modelo, nem o arquivo seriam excluídos mantendo os dados consistentes. Por favor, corrija-me se minha compreensão post_deletee pre_deletesinais estiverem errados.
toto_tico
9
Observe que isso não exclui o arquivo antigo se você substituir o arquivo em uma instância de modelo
Marcar em
3
Isso não funciona para mim no Django 1.8 fora do admin. Existe uma nova maneira de fazer isso?
califa
impressionante. estava procurando por isso há muito tempo
RL Shyam
46

Experimente django-cleanup

pip install django-cleanup

settings.py

INSTALLED_APPS = (
    ...
    'django_cleanup', # should go after your apps
)
un1t
fonte
1
Pacote muito incrível. Obrigado! :)
BoJack Horseman
3
Após testes limitados, posso confirmar que este pacote ainda funciona para Django 1.10.
CoderGuy123
1
Legal, isso é tão fácil
Tunn
Agradável. Funciona para mim no Django 2.0. Também estou usando o S3 como back-end de armazenamento ( django-storages.readthedocs.io/en/latest/backends/… ) e estou felizmente excluindo arquivos do S3.
routeburn
35

Solução Django 1.5: Eu uso post_delete por vários motivos internos ao meu aplicativo.

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)

Coloquei isso na parte inferior do arquivo models.py.

o original_imagecampo é o ImageFieldno meu Photomodelo.

Kushal
fonte
7
Para qualquer pessoa que use o Amazon S3 como back-end de armazenamento (via django-storages), esta resposta específica não funcionará. Você obterá um. NotImplementedError: This backend doesn't support absolute paths.Você pode corrigir isso facilmente passando o nome do campo do arquivo para em storage.delete()vez do caminho do campo do arquivo. Por exemplo, substitua as duas últimas linhas desta resposta por storage, name = photo.original_image.storage, photo.original_image.nameentão storage.delete(name).
Sean Azlin
2
@Sean +1, estou usando esse ajuste em 1.7 para excluir miniaturas geradas pelo django-imagekit no S3 via django-storages. docs.djangoproject.com/en/dev/ref/files/storage/… . Nota: Se você está simplesmente usando um ImageField (ou FileField), pode usar mymodel.myimagefield.delete(save=False). docs.djangoproject.com/en/dev/ref/files/file/…
user2616836
@ user2616836 Você pode usar mymodel.myimagefield.delete(save=False)no post_delete? Em outras palavras, posso ver que posso excluir o arquivo, mas você pode excluir o arquivo quando um modelo que tem o imagefield é excluído?
Eugene
1
@eugene Sim, pode, funciona (mas não tenho certeza do motivo). Em post_deletevocê instance.myimagefield.delete(save=False), observe o uso de instance.
user2616836
17

Este código funciona bem no Django 1.4 também com o painel de administração.

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 obter o armazenamento e o caminho antes de excluir o modelo, ou o último persistirá vazio também se excluído.

Davide Muzzarelli
fonte
3
Isso não funciona para mim (Django 1.5) e o CHANGELOG do Django 1.3 afirma: "No Django 1.3, quando um modelo é excluído, o método delete () do FileField não será chamado. Se você precisar limpar os arquivos órfãos, você ' Precisarei 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). "
darrinm
4
Esta solução está errada! deletenem sempre é chamado quando uma linha é excluída, você deve usar sinais.
lvella
14

Você precisa remover o arquivo real em 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)
un33k
fonte
Ótima solução!
AlexKh de
6

Você pode considerar o uso de um sinal pre_delete ou post_delete:

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

Obviamente, os mesmos motivos pelos quais a exclusão automática do FileField foi removida também se aplicam aqui. Se você deletar um arquivo referenciado em outro lugar, terá problemas.

No meu caso, isso parecia apropriado porque eu tinha um modelo de arquivo dedicado para gerenciar todos os meus arquivos.

Nota: Por algum motivo, post_delete não parece funcionar direito. O arquivo foi excluído, mas o registro do banco de dados permaneceu, o que é completamente o oposto do que eu esperava, mesmo em condições de erro. pre_delete funciona bem.

SystemParadox
fonte
3
provavelmente post_deletenão funcionará, porque file_field.delete()por padrão salva o modelo no banco de dados, tente file_field.delete(False) docs.djangoproject.com/en/1.3/ref/models/fields/…
Adam Jurczyk
3

Talvez seja um pouco tarde. Mas a maneira mais fácil para mim é usar um sinal post_save. Apenas para lembrar que os sinais são executados mesmo durante um processo de exclusão de QuerySet, mas o método [model] .delete () não é executado durante o processo de exclusão de QuerySet, portanto, não é a melhor opção para substituí-lo.

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 / signs.py

import os

def delete_image_slide(sender, **kwargs):
    slide = kwargs.get('instance')
    try:
        os.remove(slide.image.path)
    except:
        pass
Mauricio
fonte
1

Essa funcionalidade será removida no Django 1.3, então eu não confiaria nela.

Você pode substituir o deletemétodo do modelo em questão para excluir o arquivo antes de remover completamente a entrada do banco de dados.

Editar:

Aqui está um exemplo rápido.

class MyModel(models.Model):

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

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

        super(MyModel, self).delete(*args, **kwargs)
Derek Reynolds
fonte
Você tem um exemplo de como usar isso em um modelo para excluir o arquivo? Estou olhando a documentação e vejo exemplos de como remover o objeto do banco de dados, mas não vejo nenhuma implementação de exclusão de arquivo.
narkeeso
2
Este método está errado porque não funciona para exclusão em massa (como o recurso 'Excluir selecionados' do administrador). Por exemplo, MyModel.objects.all()[0].delete()irá deletar o arquivo enquanto MyModel.objects.all().delete()não. Use sinais.
Antony Hatchkins de
1

Usar o post_delete é com certeza o caminho certo a seguir. Às vezes, embora as coisas possam dar errado e os arquivos não sejam excluídos. É claro que você tem um monte de arquivos antigos que não foram excluídos antes de post_delete ser usado. Eu criei uma função que exclui arquivos para objetos com base em se o arquivo ao qual o objeto faz referência não existe, exclua o objeto, se o arquivo não tiver um objeto, exclua também, também pode excluir com base em um sinalizador "ativo" para um objeto .. Algo que adicionei à maioria dos meus modelos. Você deve passar para ele os objetos que deseja verificar, o caminho para os arquivos de objetos, o campo do arquivo e um sinalizador para excluir objetos inativos:

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

É assim que você pode usar:

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
Radtek
fonte
0

certifique-se de escrever " self " antes do arquivo. então o exemplo acima deve ser

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

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

Esqueci o "self" antes do meu arquivo e isso não funcionou porque estava procurando no namespace global.

Bjorn
fonte
0

Se você já tem vários arquivos não usados ​​em seu projeto e deseja excluí-los, você pode usar o utilitário django django-unused-media

Andrey Kolpakov
fonte
0

Solução Django 2.x:

Não há necessidade de instalar nenhum pacote! É muito fácil de manusear no Django 2 . Tentei seguir a solução usando Django 2 e SFTP Storage (no entanto, acho que funcionaria com qualquer armazenamento)

Primeiro escreva um gerenciador personalizado . Portanto, se você deseja excluir arquivos de um modelo usando objectsmétodos, deve escrever e usar um [Custom Manager] [3] (para substituir o delete()método de objects):

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

Agora você deve excluir imageantes de excluir o próprio modelo e para atribuir o CustomManagerao modelo, você deve inicializar objectsdentro de seu modelo:

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()
Hamidreza
fonte
-1

Posso ter um caso especial, pois estou usando a opção upload_to em meu campo de arquivo com nomes de diretório dinâmico, mas a solução que encontrei foi usar os.rmdir.

Nos modelos:

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)
Carruthd
fonte
1
Esta é uma ideia muito ruim. Você não apenas removerá um diretório inteiro em vez de um único arquivo (potencialmente afetando outros arquivos), mas também o fará mesmo se a exclusão do objeto real falhar.
tbm
Não é uma má ideia se você estava trabalhando no problema que eu tive;) Como mencionei, tive um caso de uso exclusivo em que o modelo sendo excluído era um modelo pai. Os filhos gravavam arquivos na pasta pai e, portanto, se você excluísse o pai, o comportamento desejado era excluir todos os arquivos da pasta. Bom ponto na ordem das operações. Isso não me ocorreu na época.
carruthd de
Eu ainda preferiria remover os arquivos filhos individuais quando um filho é excluído; então, se necessário, você pode remover o diretório pai quando ele estiver vazio.
tbm
Isso faz sentido, pois você está retirando objetos filhos, mas se o objeto pai for destruído, passar pelos filhos um de cada vez parece tedioso e desnecessário. Independentemente disso, vejo agora que a resposta que dei não foi específica o suficiente para a pergunta do OP. Obrigado pelos comentários, você me fez pensar em usar um instrumento menos contundente daqui para frente.
carruthd