Jak sprawić, by Django Admin usuwał pliki, gdy usuwam obiekt z bazy danych / modelu?

85

Używam 1.2.5 ze standardowym ImageField i używam wbudowanego zaplecza pamięci. Przesyłanie plików jest prawidłowe, ale kiedy usuwam wpis od administratora, rzeczywisty plik na serwerze nie jest usuwany.

narkeeso
źródło
Hm, właściwie powinno. Sprawdź uprawnienia do plików w folderze przesyłania (zmień na 0777).
Torsten Engelbrecht
5
Django usunął funkcję automatycznego usuwania (dla pracowników Google, którzy widzą powyższy komentarz).
Mark

Odpowiedzi:

100

Możesz odebrać sygnał pre_deletelub post_delete(patrz komentarz @ toto_tico poniżej) i wywołać metodę delete () na obiekcie FileField, a więc (w 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
źródło
10
Pamiętaj, aby dodać sprawdzenie, czy instance.filepole nie jest puste lub może (przynajmniej spróbować) usunąć cały katalog MEDIA_ROOT. Dotyczy to nawet ImageField(null=False)pól.
Antony Hatchkins
47
Dzięki. Generalnie zalecałbym użycie post_deletesygnału, ponieważ jest to bezpieczniejsze w przypadku niepowodzenia usuwania z jakiegokolwiek powodu. Wtedy ani model, ani plik nie zostaną usunięte, co zapewni spójność danych. Proszę mnie poprawić, jeśli moje zrozumienie post_deletei pre_deletesygnały są błędne.
toto_tico
9
Zwróć uwagę, że nie spowoduje to usunięcia starego pliku, jeśli zastąpisz go w instancji modelu
Mark
3
To nie działa dla mnie w Django 1.8 poza administratorem. Czy jest na to nowy sposób?
kalif
niesamowite. szukałem tego przez długi czas
RL Shyam
46

Spróbuj django-cleanup

pip install django-cleanup

settings.py

INSTALLED_APPS = (
    ...
    'django_cleanup', # should go after your apps
)
un1t
źródło
1
Bardzo fajny pakiet. Dziękuję Ci! :)
BoJack Horseman
3
Po ograniczonych testach mogę potwierdzić, że ten pakiet nadal działa dla Django 1.10.
CoderGuy123
1
Fajnie, to takie proste
Tunn
Ładny. U mnie działa na Django 2.0. Używam również S3 jako mojego zaplecza do przechowywania ( django-storages.readthedocs.io/en/latest/backends/... ) i szczęśliwie usuwa pliki z S3.
powrót do
35

Rozwiązanie Django 1.5: używam post_delete z różnych powodów wewnętrznych dla mojej aplikacji.

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)

Umieściłem to na dole pliku models.py.

original_imagepole jest ImageFieldw moim Photomodelu.

Kushal
źródło
7
Dla każdego, kto używa Amazon S3 jako zaplecza pamięci masowej (przez django-storages), ta konkretna odpowiedź nie zadziała. Otrzymasz komunikat NotImplementedError: This backend doesn't support absolute paths.Możesz łatwo to naprawić, przekazując nazwę pola pliku storage.delete()zamiast ścieżki do pola pliku. Na przykład zamień ostatnie dwa wiersze tej odpowiedzi na storage, name = photo.original_image.storage, photo.original_image.namewtedy storage.delete(name).
Sean Azlin
2
@Sean +1, używam tego dostosowania w wersji 1.7, aby usunąć miniatury wygenerowane przez django-imagekit na S3 za pośrednictwem django-storages. docs.djangoproject.com/en/dev/ref/files/storage/… . Uwaga: jeśli po prostu używasz ImageField (lub FileField), możesz użyć mymodel.myimagefield.delete(save=False)zamiast niego. docs.djangoproject.com/en/dev/ref/files/file/ ...
user2616836
@ user2616836 Czy możesz używać mymodel.myimagefield.delete(save=False)na post_delete? Innymi słowy, widzę, że mogę usunąć plik, ale czy możesz usunąć plik, gdy model z polem obrazu zostanie usunięty?
Eugene
1
@eugene Tak, możesz, to działa (nie jestem pewien dlaczego). W post_deletetym celu instance.myimagefield.delete(save=False)zwróć uwagę na użycie instance.
user2616836
17

Ten kod działa dobrze w Django 1.4 również z panelem administracyjnym.

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)

Ważne jest, aby uzyskać miejsce do przechowywania i ścieżkę przed usunięciem modelu, w przeciwnym razie ten ostatni pozostanie nieważny również po usunięciu.

Davide Muzzarelli
źródło
3
To nie działa dla mnie (Django 1.5), a Django 1.3 CHANGELOG stwierdza: "W Django 1.3, kiedy model zostanie usunięty, metoda delete () nie zostanie wywołana. Jeśli potrzebujesz wyczyścić osierocone pliki, to ' Będę musiał sam sobie z tym poradzić (na przykład za pomocą niestandardowego polecenia zarządzania, które można uruchamiać ręcznie lub zaplanować okresowe uruchamianie za pośrednictwem np. crona). ”
darrinm,
4
To rozwiązanie jest złe! deletenie zawsze jest wywoływana po usunięciu wiersza, musisz użyć sygnałów.
lvella
11

Ci potrzeba , aby usunąć rzeczywisty plik na obu deletei 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
źródło
Świetne rozwiązanie!
AlexKh
6

Możesz rozważyć użycie sygnału pre_delete lub post_delete:

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

Oczywiście te same powody, dla których usunięto automatyczne usuwanie FileField, mają również zastosowanie tutaj. Jeśli usuniesz plik, do którego istnieją odniesienia w innym miejscu, będziesz mieć problemy.

W moim przypadku wydawało się to właściwe, ponieważ miałem dedykowany model File do zarządzania wszystkimi moimi plikami.

Uwaga: z jakiegoś powodu post_delete nie działa prawidłowo. Plik został usunięty, ale rekord bazy danych pozostał, co jest całkowitym przeciwieństwem tego, czego bym się spodziewał, nawet w przypadku wystąpienia błędów. pre_delete działa jednak dobrze.

SystemParadox
źródło
3
prawdopodobnie post_deletenie zadziała, bo file_field.delete()domyślnie zapisuje model do db, spróbuj file_field.delete(False) docs.djangoproject.com/en/1.3/ref/models/fields/…
Adam Jurczyk
3

Może jest trochę za późno. Ale najłatwiejszym sposobem jest użycie sygnału post_save. Wystarczy pamiętać, że sygnały są wykonywane nawet podczas procesu usuwania QuerySet, ale metoda [model] .delete () nie jest wykonywana podczas procesu usuwania QuerySet, więc nie jest to najlepsza opcja, aby ją zastąpić.

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

rdzeń / sygnały.py

import os

def delete_image_slide(sender, **kwargs):
    slide = kwargs.get('instance')
    try:
        os.remove(slide.image.path)
    except:
        pass
Mauricio
źródło
1

Ta funkcjonalność zostanie usunięta w Django 1.3, więc nie będę na niej polegać.

Możesz zmienić deletemetodę danego modelu, aby usunąć plik przed całkowitym usunięciem wpisu z bazy danych.

Edytować:

Oto szybki przykład.

class MyModel(models.Model):

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

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

        super(MyModel, self).delete(*args, **kwargs)
Derek Reynolds
źródło
Czy masz przykład, jak użyć tego w modelu, aby usunąć plik? Patrzę na dokumenty i widzę przykłady, jak usunąć obiekt z bazy danych, ale nie widzę żadnych implementacji dotyczących usuwania plików.
narkeeso
2
Ta metoda jest nieprawidłowa, ponieważ nie będzie działać w przypadku usuwania zbiorczego (jak funkcja administratora „Usuń wybrane”). Na przykład MyModel.objects.all()[0].delete()usunie plik, ale MyModel.objects.all().delete()nie. Użyj sygnałów.
Antony Hatchkins
1

Korzystanie z post_delete jest z pewnością właściwą drogą. Czasami jednak coś może pójść nie tak, a pliki nie są usuwane. Oczywiście jest tak, że masz kilka starych plików, które nie zostały usunięte przed użyciem post_delete. Stworzyłem funkcję, która usuwa pliki dla obiektów w oparciu o to, czy plik, do którego obiekt się odwołuje, nie istnieje, a następnie usuwa obiekt, jeśli plik nie ma obiektu, a następnie usuwa, również może usuwać na podstawie flagi „aktywny” dla obiekt .. Coś, co dodałem do większości moich modeli. Musisz przekazać mu obiekty, które chcesz sprawdzić, ścieżkę do plików obiektów, pole pliku i flagę do usuwania nieaktywnych obiektów:

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

Oto jak możesz tego użyć:

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
źródło
0

upewnij się, że przed plikiem wpisałeś „ siebie ”. więc przykład powyżej powinien być

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

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

Zapomniałem o "self" przed moim plikiem i to nie działało, ponieważ wyglądało w globalnej przestrzeni nazw.

Bjorn
źródło
0

Rozwiązanie Django 2.x:

Nie ma potrzeby instalowania żadnych pakietów! Jest bardzo łatwy w obsłudze w Django 2 . Próbowałem następującego rozwiązania przy użyciu Django 2 i SFTP Storage (jednak myślę, że działałoby z dowolnymi magazynami)

Najpierw napisz Custom Managera . Więc jeśli chcesz mieć możliwość usuwania plików modelu przy użyciu objectsmetod, musisz napisać i użyć [Custom Manager] [3] (do nadpisywania delete()metody objects):

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

Teraz musisz usunąć, imagezanim usuniesz sam model i aby przypisać CustomManagerdo modelu, musisz zainicjować objectswewnątrz swojego modelu:

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
źródło
-1

Mogę mieć specjalny przypadek, ponieważ używam opcji upload_to w moim polu pliku z dynamicznymi nazwami katalogów, ale rozwiązaniem, które znalazłem, było użycie os.rmdir.

W modelach:

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
źródło
1
To bardzo zły pomysł. Nie tylko usuniesz cały katalog zamiast pojedynczego pliku (potencjalnie wpływając na inne pliki), zrobisz to nawet jeśli faktyczne usunięcie obiektu nie powiedzie się.
tbm
To nie jest zły pomysł, jeśli pracujesz nad problemem, który miałem;) Jak wspomniałem, miałem unikalny przypadek użycia, w którym usuwany model był modelem macierzystym. Dzieci zapisywały pliki w folderze nadrzędnym, więc jeśli usunąłeś element nadrzędny, pożądanym zachowaniem było usunięcie wszystkich plików w folderze. Dobra uwaga co do kolejności operacji. Wtedy nie przyszło mi to do głowy.
carruthd
Nadal wolałbym usuwać poszczególne pliki podrzędne, gdy dziecko jest usuwane; wtedy jeśli potrzebujesz, możesz usunąć katalog nadrzędny, gdy jest pusty.
tbm
Ma to sens, ponieważ wyjmujesz obiekty podrzędne, ale jeśli obiekt nadrzędny zostanie zniszczony, przechodzenie między dziećmi pojedynczo wydaje się nudne i niepotrzebne. Niezależnie od tego, teraz widzę, że odpowiedź, której udzieliłem, nie była wystarczająco konkretna na pytanie OP. Dziękuję za komentarze, kazałeś mi pomyśleć o użyciu mniej tępego instrumentu w przyszłości.
carruthd