Django usuń FileField

93

Buduję aplikację internetową w Django. Mam model, który przesyła plik, ale nie mogę go usunąć. Oto mój kod:

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)

Następnie w „python manage.py shell” robię to:

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

Usuwa z bazy danych, ale nie usuwa pliku na serwerze. Czego jeszcze mogę spróbować?

Dzięki!

Marcos Aguayo
źródło
A co z bezpośrednim korzystaniem z default_storage? docs.djangoproject.com/en/dev/topics/files
MGP

Odpowiedzi:

142

Przed Django 1.3 plik był usuwany z systemu plików automatycznie po usunięciu odpowiedniej instancji modelu. Prawdopodobnie używasz nowszej wersji Django, więc będziesz musiał samodzielnie zaimplementować usuwanie pliku z systemu plików.

Możesz to zrobić na kilka sposobów, z których jednym jest użycie sygnału pre_deletelub post_delete.

Przykład

Moją metodą z wyboru jest obecnie mieszanka sygnałów post_deletei pre_save, co sprawia, że ​​przestarzałe pliki są usuwane po każdym usunięciu odpowiednich modeli lub zmianie ich plików.

Na podstawie hipotetycznego MediaFilemodelu:

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)
  • Sytuacja skrajna: jeśli Twoja aplikacja prześle nowy plik i wskaże instancję modelu do nowego pliku bez wywoływania save()(np. Poprzez zbiorczą aktualizację a QuerySet), stary plik będzie nadal leżał, ponieważ sygnały nie będą uruchamiane. Nie dzieje się tak, jeśli używasz konwencjonalnych metod obsługi plików.
  • Myślę, że jedna z aplikacji, które zbudowałem, ma ten kod w wersji produkcyjnej, ale mimo to używasz go na własne ryzyko.
  • Styl kodowania: w tym przykładzie filenazwa pola jest używana , co nie jest dobrym stylem, ponieważ koliduje z wbudowanym fileidentyfikatorem obiektu.

Zobacz też

  • FieldFile.delete()w modelu Django 1.11 odwołanie do pola (zwróć uwagę, że opisuje FieldFileklasę, ale wywołałbyś .delete()bezpośrednio w polu: FileFieldinstancje proxy do odpowiedniej FieldFileinstancji i uzyskujesz dostęp do jej metod tak, jakby były polami)

    Należy pamiętać, że po usunięciu modelu powiązane pliki nie są usuwane. Jeśli chcesz wyczyścić osierocone pliki, musisz to zrobić samodzielnie (na przykład za pomocą niestandardowego polecenia zarządzania, które można uruchamiać ręcznie lub zaplanować okresowe uruchamianie przez np. Cron).

  • Dlaczego Django nie usuwa plików automatycznie: wpis w informacjach o wydaniu dla Django 1.3

    We wcześniejszych wersjach Django, gdy instancja modelu zawierająca element a FileFieldzostała usunięta, FileFieldpodjęło się również usunięcia pliku z pamięci wewnętrznej. Otworzyło to drzwi do kilku scenariuszy utraty danych, w tym wycofanych transakcji i pól w różnych modelach odwołujących się do tego samego pliku. W Django 1.3, kiedy model zostanie usunięty FileField, delete()metoda nie zostanie wywołana. Jeśli potrzebujesz wyczyścić osierocone pliki, będziesz musiał sobie z tym poradzić (na przykład za pomocą niestandardowego polecenia zarządzania, które można uruchomić ręcznie lub zaplanować okresowe uruchamianie przez np. Cron).

  • Przykład wykorzystania samego pre_deletesygnału

Anton Strogonoff
źródło
2
Tak, ale upewnij się, że wykonałeś odpowiednie kontrole. (Daj mi chwilę, prześlę kod, który znalazłem w używanym systemie.)
Anton Strogonoff Kwietnia
7
Prawdopodobnie jest lepszy w użyciu instance.song.delete(save=False), ponieważ używa prawidłowego silnika pamięci masowej django.
Eduardo
1
W dzisiejszych czasach rzadko kiedy kopiuję kod, którego sam nie byłbym w stanie napisać bezpośrednio z SO i działa z ograniczonymi modyfikacjami. Fantastyczna pomoc, dzięki!
GJStein
Znalazłem błąd w tym, że jeśli instancja istnieje, ale żaden obraz nie został wcześniej zapisany, to os.path.isfile(old_file.path)kończy się niepowodzeniem, ponieważ old_file.pathpowoduje błąd (żaden plik nie jest powiązany z polem). Naprawiłem to, dodając if old_file:tuż przed wywołaniem os.path.isfile().
three_pineapples
@three_pineapples ma sens. Może się zdarzyć, że ograniczenie NOT NULL w polu pliku zostało ominięte lub nie zostało zakończone w pewnym momencie, w którym to przypadku niektóre obiekty miałyby je puste.
Anton Strogonoff,
78

Wypróbuj django-cleanup , automatycznie wywołuje metodę delete na FileField po usunięciu modelu.

pip install django-cleanup

settings.py

INSTALLED_APPS = (
     ...
    'django_cleanup', # should go after your apps
)
un1t
źródło
Super, domyślnie musi być dodany do FileField, dzięki!
megajoe
Usuwa
Łał. Starałem się, żeby to się nie stało i nie mogłem zrozumieć, dlaczego tak się dzieje. Ktoś zainstalował to lata temu i zapomniał o tym. Dzięki.
ryan28561
4
Dlaczego więc Django w pierwszej kolejności usunęło funkcję usuwania pola plików?
ha-neul
Jesteś legendą !!
marlonjd
32

Możesz usunąć plik z systemu plików za pomocą .deletemetody wywołania pola pliku pokazanej poniżej z Django> = 1.10:

obj = Song.objects.get(pk=1)
obj.song.delete()
Mesut Tasci
źródło
7
Powinna być akceptowana odpowiedź, prosta i po prostu działa.
Nikolay Shindarov
14

Możesz także po prostu nadpisać funkcję usuwania modelu, aby sprawdzić, czy plik istnieje, i usunąć go przed wywołaniem funkcji 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
źródło
8
Uważaj, ponieważ dzwonienie queryset.delete()nie wyczyści plików za pomocą tego rozwiązania. Będziesz musiał iterować zestaw zapytań i wywołać .delete()każdy obiekt.
Scott Woodall
Jestem nowy w Django. To dobrze, ale co by było, gdyby model dziedziczył po klasie abstrakcyjnej, która nadpisała metodę delete, czy nie zastąpi to tego z klasy abstrakcyjnej? Korzystanie z sygnałów wydaje mi się lepsze
TyPan
8

Rozwiązanie Django 2.x:

Usuwanie plików w Django 2 jest bardzo łatwe . Wypróbowałem następujące rozwiązanie wykorzystujące Django 2 i SFTP Storage, a także FTP STORAGE i jestem prawie pewien, że będzie działać z każdym innym menedżerem pamięci, który zaimplementował tę deletemetodę. ( deletemetoda jest jedną zstorage metod abstrakcyjnych).

Zastąp deletemetodę modelu w taki sposób, że instancja usuwa swoje FileFields przed usunięciem siebie:

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

U mnie działa to całkiem łatwo. Jeśli chcesz sprawdzić, czy plik istnieje przed usunięciem, możesz użyć storage.exists. np. self.song.storage.exists(self.song.name)zwróci booleanreprezentację, jeśli piosenka istnieje. Więc będzie to wyglądać tak:

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

EDYCJA (dodatkowo):

Jak wspomniał @HeyMan , wywołanie tego rozwiązania Song.objects.all().delete()nie powoduje usunięcia plików! Dzieje się tak, ponieważ Song.objects.all().delete()jest uruchomione zapytanie usuwające Menedżera domyślnego . Więc jeśli chcesz mieć możliwość usuwania plików modelu za pomocą objectsmetod, musisz napisać i używać Custom Manager (tylko do nadpisania jego zapytania usuwającego):

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

i aby przypisać CustomManagerdo modelu, musisz zainicjować objectswewnątrz swojego modelu:

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

Teraz możesz użyć .delete()na końcu dowolnych objectspod-zapytań. Napisałem najprostszy CustomManager, ale możesz to zrobić lepiej, zwracając coś o usuniętych obiektach lub cokolwiek chcesz.

Hamidreza
źródło
1
Tak, myślę, że dodali tę funkcję, odkąd opublikowałem pytanie.
Marcos Aguayo
1
Nadal usuwanie nie jest nazywane wenn wywołującym funkcję Song.objects.all (). Delete (). To samo dotyczy sytuacji, gdy Instancja zostanie usunięta przez on_delete = models.CASCADE.
HeyMan
@HeyMan Już to rozwiązałem i zredagowałem moje rozwiązanie :)
Hamidreza
4

Oto aplikacja, która usunie stare pliki po usunięciu modelu lub załadowaniu nowego pliku: 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
źródło
3

@Anton Strogonoff

Brakuje mi czegoś w kodzie, gdy zmiana pliku, jeśli tworzysz nowy plik generuje błąd, ponieważ jest to nowy plik i nie znalazłem ścieżki. Zmodyfikowałem kod funkcji i dodałem zdanie try / except i działa dobrze.

@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
Programista Java
źródło
Nie spotkałem się z tym - może to być błąd w moim kodzie lub coś się zmieniło w Django. Sugerowałbym jednak złapanie konkretnego wyjątku w twoim try:bloku ( AttributeErrorbyć może?).
Anton Strogonoff
Korzystanie z biblioteki OS nie jest dobrym pomysłem, ponieważ podczas migracji do innego magazynu (na przykład Amazon S3) wystąpią problemy.
Igor Pomaranskiy
@IgorPomaranskiy co by się stało w magazynie takim jak Amazon S3, gdy używasz os.remove ??
Daniel González Fernández
@ DanielGonzálezFernández Myślę, że to się nie powiedzie (z błędem jak coś o nieistniejącej ścieżce). Dlatego Django używa abstrakcji do magazynów.
Igor Pomaranskiy
0

Ten kod będzie uruchamiany za każdym razem, gdy załaduję nowy obraz (pole logo) i sprawdzę, czy logo już istnieje, jeśli tak, zamknij je i usuń z dysku. Ta sama procedura mogłaby być oczywiście wykonana w funkcji odbiornika. Mam nadzieję że to pomoże.

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