Django menghapus FileField


93

Saya sedang membangun aplikasi web di Django. Saya memiliki model yang mengupload file, tetapi saya tidak dapat menghapusnya. Ini kode saya:

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)

Kemudian, di "python manage.py shell" saya melakukan ini:

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

Ini menghapus dari database tetapi bukan file di server. Apa lagi yang bisa saya coba?

Terima kasih!


Bagaimana dengan menggunakan default_storage secara langsung? docs.djangoproject.com/en/dev/topics/files
MGP

Jawaban:


142

Sebelum Django 1.3, berkas telah dihapus dari sistem berkas secara otomatis ketika Anda menghapus contoh model terkait. Anda mungkin menggunakan versi Django yang lebih baru, jadi Anda harus menerapkan menghapus berkas dari sistem berkas sendiri.

Anda dapat melakukannya dengan beberapa cara, salah satunya dengan menggunakan sinyal pre_deleteatau post_delete.

Contoh

Metode pilihan saya saat ini adalah campuran post_deletedan pre_savesinyal, yang membuatnya sehingga file usang akan dihapus setiap kali model terkait dihapus atau file mereka diubah.

Berdasarkan MediaFilemodel hipotetis :

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)
  • Kasus edge: jika aplikasi Anda mengupload file baru dan menunjukkan instance model ke file baru tanpa memanggil save()(misalnya dengan mengupdate massal a QuerySet), file lama akan tetap ada karena sinyal tidak akan dijalankan. Ini tidak terjadi jika Anda menggunakan metode penanganan file konvensional.
  • Saya pikir salah satu aplikasi yang saya buat memiliki kode ini dalam produksi tetapi tetap menggunakannya dengan risiko Anda sendiri.
  • Gaya pengkodean: contoh ini digunakan filesebagai nama bidang, yang bukan merupakan gaya yang baik karena bentrok dengan filepengenal objek bawaan.

Lihat juga

  • FieldFile.delete()dalam referensi bidang model Django 1.11 (perhatikan bahwa ini menjelaskan FieldFilekelas, tetapi Anda akan memanggil .delete()langsung pada bidang: FileFieldproxy instance ke FieldFileinstance terkait , dan Anda mengakses metodenya seolah-olah mereka adalah bidang)

    Perhatikan bahwa ketika model dihapus, file terkait tidak dihapus. Jika Anda perlu membersihkan file yatim piatu, Anda harus menanganinya sendiri (misalnya, dengan perintah manajemen kustom yang dapat dijalankan secara manual atau dijadwalkan untuk dijalankan secara berkala melalui mis. Cron).

  • Mengapa Django tidak menghapus berkas secara otomatis: entri dalam catatan terbitan untuk Django 1.3

    Dalam versi Django sebelumnya, ketika sebuah contoh model yang berisi sebuah FileFieldtelah dihapus, FileFieldmengambilnya sendiri untuk juga menghapus berkas dari penyimpanan backend. Ini membuka pintu ke beberapa skenario kehilangan data, termasuk transaksi yang dibatalkan dan bidang pada model berbeda yang mereferensikan file yang sama. Dalam Django 1.3, ketika model dihapus dengan FileField's delete()metode tidak akan dipanggil. Jika Anda memerlukan pembersihan file yatim piatu, Anda harus menanganinya sendiri (misalnya, dengan perintah manajemen kustom yang dapat dijalankan secara manual atau dijadwalkan untuk dijalankan secara berkala melalui mis. Cron).

  • Contoh penggunaan pre_deletesinyal saja


2
Ya, tapi pastikan untuk melakukan pemeriksaan yang sesuai. (Beri saya waktu sebentar, saya akan memposting kode yang saya temukan digunakan dalam sistem yang sebenarnya.)
Anton Strogonoff

7
Ini mungkin lebih baik untuk digunakan instance.song.delete(save=False), karena menggunakan mesin penyimpanan django yang benar.
Eduardo

1
Jarang saat ini saya menyalin kode, saya tidak akan dapat menulis sendiri langsung dari SO dan bekerja dengan modifikasi terbatas. Bantuan yang luar biasa, terima kasih!
GJStein

Menemukan bug di sini di mana jika ada, tetapi tidak ada gambar yang disimpan sebelumnya, kemudian os.path.isfile(old_file.path)gagal karena old_file.pathmenimbulkan kesalahan (tidak ada file yang terkait dengan bidang). Saya memperbaikinya dengan menambahkan if old_file:sebelum panggilan ke os.path.isfile().
three_pineapples

@tiga_bisnis masuk akal. Bisa jadi batasan NOT NULL pada bidang file telah dilewati atau tidak keluar di beberapa titik, dalam hal ini beberapa objek akan mengosongkannya.
Anton Strogonoff

78

Coba django-cleanup , ini secara otomatis memanggil metode delete pada FileField ketika Anda menghapus model.

pip install django-cleanup

settings.py

INSTALLED_APPS = (
     ...
    'django_cleanup', # should go after your apps
)

Keren, itu perlu ditambahkan ke FileField secara default, terima kasih!
megajoe

Itu menghapus file saat mengunggah juga
chirag soni

Wow. Saya mencoba agar ini tidak terjadi dan saya tidak tahu mengapa itu terjadi. Seseorang telah menginstal ini bertahun-tahun yang lalu dan melupakannya. Terima kasih.
ryan28561

4
Jadi, mengapa Django menghapus fungsi hapus filefield di tempat pertama?
ha-neul

Kamu adalah sang legenda !!
marlonjd

32

Anda dapat menghapus berkas dari sistem berkas dengan .deletemetode pemanggilan bidang berkas yang ditunjukkan seperti di bawah ini dengan Django> = 1.10:

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

7
Harus menjadi jawaban yang diterima, sederhana dan berhasil.
Nikolay Shindarov

14

Anda juga dapat dengan mudah menimpa fungsi delete dari model untuk memeriksa file apakah ada dan menghapusnya sebelum memanggil fungsi 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)

8
Berhati-hatilah karena panggilan queryset.delete()tidak akan membersihkan file dengan solusi ini. Anda perlu mengulang queryset dan memanggil .delete()setiap objek.
Scott Woodall

Saya baru mengenal Django. Ini bagus, tetapi bagaimana jika model itu mewarisi dari kelas abstrak yang telah menimpa metode hapus, bukankah ini akan menimpanya dari kelas abstrak? Menggunakan sinyal tampak lebih baik bagi saya
theTypan

8

Solusi Django 2.x:

Sangat mudah menangani penghapusan file di Django 2 . Saya telah mencoba mengikuti solusi menggunakan Django 2 dan Penyimpanan SFTP dan juga PENYIMPANAN FTP, dan saya cukup yakin bahwa ini akan bekerja dengan manajer penyimpanan lain yang menerapkan deletemetode. ( deletemetode adalah salah satu storagemetode abstrak.)

Ganti deletemetode model sedemikian rupa sehingga instance menghapus FileFields sebelum menghapusnya sendiri:

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

Ini bekerja sangat mudah bagi saya. Jika Anda ingin memeriksa apakah file sudah ada sebelum dihapus, Anda dapat menggunakan storage.exists. mis. self.song.storage.exists(self.song.name)akan mengembalikan booleanrepresentasi jika lagu tersebut ada. Jadi akan terlihat seperti ini:

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 (Sebagai Tambahan):

Seperti yang disebutkan @HeyMan , dengan solusi ini, pemanggilan Song.objects.all().delete()tidak menghapus file! Ini terjadi karena Song.objects.all().delete()menjalankan query delete dari Default Manager . Jadi, jika Anda ingin menghapus file model dengan menggunakan objectsmetode, Anda harus menulis dan menggunakan Manajer Kustom (hanya untuk menimpa kueri penghapusannya):

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

dan untuk menetapkan CustomManagerke model, Anda harus melakukan inisial objectsdi dalam model Anda:

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

Sekarang Anda dapat menggunakan .delete()di akhir objectssub-kueri apa pun . Saya menulis yang paling sederhana CustomManager, tetapi Anda dapat melakukannya lebih baik dengan mengembalikan sesuatu tentang objek yang Anda hapus atau apa pun yang Anda inginkan.


1
Ya, saya pikir mereka menambahkan fitur itu sejak saya memposting pertanyaan.
Marcos Aguayo

1
Masih hapus tidak disebut wenn memanggil Song.objects.all (). Delete (). Sama seperti saat Instance dihapus oleh on_delete = models.CASCADE.
HeyMan

@HeyMan Saya menyelesaikannya dan mengedit solusi saya sekarang :)
Hamidreza

4

Berikut adalah aplikasi yang akan menghapus file lama setiap kali model dihapus atau file baru diunggah: 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)

3

@Anton Strogono

Saya kehilangan sesuatu dalam kode ketika file berubah, jika Anda membuat file baru menghasilkan kesalahan, karena file baru tidak menemukan jalan. Saya memodifikasi kode fungsi dan menambahkan kalimat coba / kecuali dan berfungsi dengan baik.

@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

Saya belum menemukan ini — bisa jadi bug dalam kode saya, atau sesuatu berubah di Django. Saya akan menyarankan menangkap pengecualian khusus di try:blok Anda , meskipun ( AttributeErrormungkin?).
Anton Strogonoff

Bukan ide yang baik untuk menggunakan perpustakaan os, karena Anda akan mengalami masalah jika Anda bermigrasi ke penyimpanan yang berbeda (Amazon S3, misalnya).
Igor Pomaranskiy

@IgorPomaranskiy apa yang akan terjadi di penyimpanan seperti Amazon S3 saat Anda menggunakan os.remove ??
Daniel González Fernández

@ DanielGonzálezFernández Saya kira itu akan gagal (dengan kesalahan seperti sesuatu tentang jalur yang tidak ada). Itulah mengapa Django menggunakan abstraksi untuk penyimpanan.
Igor Pomaranskiy

0

Kode ini akan berjalan setiap kali saya mengunggah gambar baru (bidang logo) dan memeriksa apakah logo sudah ada, jika demikian, tutup dan hapus dari disk. Prosedur yang sama tentu saja dapat dilakukan dalam fungsi penerima. Semoga ini membantu.

 #  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='')
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.