Django ลบ FileField


93

ฉันกำลังสร้างเว็บแอปใน Django ฉันมีโมเดลที่อัปโหลดไฟล์ แต่ไม่สามารถลบได้ นี่คือรหัสของฉัน:

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)

จากนั้นใน "python Manage.py shell" ฉันทำสิ่งนี้:

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

มันลบออกจากฐานข้อมูล แต่ไม่ใช่ไฟล์บนเซิร์ฟเวอร์ ฉันจะลองทำอะไรได้อีก?

ขอบคุณ!


แล้วการใช้ default_storage โดยตรงล่ะ docs.djangoproject.com/th/dev/topics/files
MGP

คำตอบ:


142

ก่อน Django 1.3 ไฟล์จะถูกลบออกจากระบบไฟล์โดยอัตโนมัติเมื่อคุณลบอินสแตนซ์โมเดลที่เกี่ยวข้อง คุณอาจใช้ Django เวอร์ชันใหม่กว่าดังนั้นคุณจะต้องดำเนินการลบไฟล์ออกจากระบบไฟล์ด้วยตัวเอง

คุณสามารถทำได้สองสามวิธีซึ่งหนึ่งในนั้นคือการใช้pre_deleteหรือpost_deleteสัญญาณ

ตัวอย่าง

วิธีที่ฉันเลือกในตอนนี้คือการผสมผสานpost_deleteและpre_saveสัญญาณซึ่งทำให้ไฟล์ที่ล้าสมัยถูกลบเมื่อใดก็ตามที่รุ่นที่เกี่ยวข้องถูกลบหรือมีการเปลี่ยนแปลงไฟล์

ตามMediaFileแบบจำลองสมมุติฐาน:

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)
  • Edge case: หากแอปของคุณอัปโหลดไฟล์ใหม่และชี้อินสแตนซ์โมเดลไปยังไฟล์ใหม่โดยไม่ต้องเรียกsave()(เช่นโดยการอัปเดต a จำนวนมากQuerySet) ไฟล์เก่าจะยังคงอยู่เฉยๆเนื่องจากสัญญาณจะไม่ทำงาน สิ่งนี้จะไม่เกิดขึ้นหากคุณใช้วิธีการจัดการไฟล์แบบเดิม
  • ฉันคิดว่าหนึ่งในแอพที่ฉันสร้างขึ้นมีรหัสนี้ในการผลิต แต่อย่างไรก็ตามการใช้งานโดยยอมรับความเสี่ยงเอง
  • รูปแบบการเข้ารหัส: ตัวอย่างนี้ใช้fileเป็นชื่อฟิลด์ซึ่งไม่ใช่ลักษณะที่ดีเนื่องจากขัดแย้งกับfileตัวระบุอ็อบเจ็กต์ในตัว

ดูสิ่งนี้ด้วย

  • FieldFile.delete()ในการอ้างอิงฟิลด์โมเดล Django 1.11 (โปรดทราบว่ามันอธิบายFieldFileคลาส แต่คุณจะเรียกใช้.delete()โดยตรงบนฟิลด์: FileFieldอินสแตนซ์พร็อกซีไปยังFieldFileอินสแตนซ์ที่เกี่ยวข้องและคุณเข้าถึงวิธีการราวกับว่ามันเป็นฟิลด์)

    โปรดทราบว่าเมื่อลบโมเดลไฟล์ที่เกี่ยวข้องจะไม่ถูกลบ หากคุณต้องการล้างข้อมูลไฟล์ที่ไม่มีเจ้าของคุณจะต้องจัดการด้วยตัวเอง (ตัวอย่างเช่นด้วยคำสั่งการจัดการแบบกำหนดเองที่สามารถเรียกใช้ด้วยตนเองหรือกำหนดเวลาให้ทำงานเป็นระยะ ๆ ผ่านเช่น cron)

  • ทำไม Django ไม่ลบไฟล์โดยอัตโนมัติ: รายการในบันทึกประจำรุ่นสำหรับ Django 1.3

    ใน Django เวอร์ชันก่อนหน้านี้เมื่ออินสแตนซ์ของโมเดลที่มี a FileFieldถูกลบFileFieldไปจะลบไฟล์ออกจากที่เก็บข้อมูลแบ็กเอนด์ด้วย สิ่งนี้เปิดประตูไปสู่สถานการณ์การสูญเสียข้อมูลหลายแบบรวมถึงธุรกรรมย้อนกลับและฟิลด์ในโมเดลต่างๆที่อ้างถึงไฟล์เดียวกัน ใน Django 1.3 เมื่อรูปแบบจะถูกลบFileField's delete()วิธีการจะไม่ถูกเรียก หากคุณต้องการล้างไฟล์ที่ไม่มีที่มาคุณจะต้องจัดการด้วยตัวเอง (ตัวอย่างเช่นด้วยคำสั่งการจัดการแบบกำหนดเองที่สามารถเรียกใช้ด้วยตนเองหรือกำหนดเวลาให้ทำงานเป็นระยะ ๆ ผ่านเช่น cron)

  • ตัวอย่างการใช้pre_deleteสัญญาณเท่านั้น


2
ได้ แต่อย่าลืมทำการตรวจสอบที่เหมาะสม (ขอเวลาฉันสัก
ครู่

7
น่าจะดีกว่าที่จะใช้instance.song.delete(save=False)เนื่องจากใช้เครื่องมือจัดเก็บ django ที่ถูกต้อง
Eduardo

1
หายากแล้วที่ฉันคัดลอกโค้ดในปัจจุบันฉันไม่สามารถเขียนด้วยตัวเองโดยตรงจาก SO และใช้งานได้กับการปรับเปลี่ยนที่ จำกัด ความช่วยเหลือที่ยอดเยี่ยมขอบคุณ!
GJStein

พบข้อบกพร่องในกรณีนี้หากมีอินสแตนซ์ แต่ไม่มีการบันทึกภาพไว้ก่อนหน้านี้จะos.path.isfile(old_file.path)ล้มเหลวเนื่องจากold_file.pathทำให้เกิดข้อผิดพลาด (ไม่มีไฟล์ที่เชื่อมโยงกับฟิลด์) ฉันคงได้โดยการเพิ่มเพียงก่อนที่การเรียกร้องให้if old_file: os.path.isfile()
three_pineapples

@three_pineapples เข้าท่า. อาจเป็นไปได้ว่าข้อ จำกัด NOT NULL บนฟิลด์ไฟล์ถูกข้ามหรือไม่ออกในบางจุดซึ่งในกรณีนี้วัตถุบางอย่างจะว่างเปล่า
Anton Strogonoff

78

ลองdjango-cleanupโดยจะเรียกใช้วิธีการลบบน FileField โดยอัตโนมัติเมื่อคุณลบโมเดล

pip install django-cleanup

settings.py

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

เจ๋งมากต้องเพิ่มลงใน FileField ตามค่าเริ่มต้นขอบคุณ!
megajoe

กำลังลบไฟล์ขณะอัปโหลดเช่นกัน
soni

ว้าว. ฉันพยายามไม่ให้สิ่งนี้เกิดขึ้นและฉันก็คิดไม่ออกว่าทำไมถึงเป็นเช่นนั้น มีคนติดตั้งเมื่อหลายปีก่อนและลืมมันไป ขอบคุณ.
ryan28561

4
เหตุใด Django จึงลบฟังก์ชัน filefield delete ตั้งแต่แรก?
ha-neul

คุณคือตำนาน !!
marlonjd

32

คุณสามารถลบไฟล์ออกจากระบบไฟล์ด้วย.deleteวิธีการเรียกฟิลด์ไฟล์ที่แสดงด้านล่างด้วย Django> = 1.10:

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

7
ควรเป็นคำตอบที่ยอมรับง่ายและใช้งานได้จริง
Nikolay Shindarov

14

คุณยังสามารถเขียนทับฟังก์ชันลบของโมเดลเพื่อตรวจสอบไฟล์ว่ามีอยู่หรือไม่และลบออกก่อนที่จะเรียกใช้ฟังก์ชันขั้นสูง

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
ระวังว่าการโทรqueryset.delete()จะไม่ล้างไฟล์ด้วยวิธีนี้ คุณจะต้องทำซ้ำในชุดแบบสอบถามและเรียก.delete()ใช้แต่ละวัตถุ
Scott Woodall

ฉันยังใหม่กับ Django นี่เป็นสิ่งที่ดี แต่จะเกิดอะไรขึ้นถ้าโมเดลได้รับการสืบทอดจากคลาสนามธรรมที่ลบล้างเมธอดการลบสิ่งนี้จะไม่แทนที่สิ่งนั้นจากคลาสนามธรรมหรือไม่? การใช้สัญญาณดูเหมือนจะดีกว่าสำหรับฉัน
theTypan

8

Django 2.x โซลูชัน:

มันง่ายมากที่จะลบการจัดการไฟล์ในDjango 2 ฉันได้ลองใช้วิธีแก้ปัญหาต่อไปนี้โดยใช้ Django 2 และ SFTP Storage และ FTP STORAGE และฉันค่อนข้างมั่นใจว่ามันจะทำงานร่วมกับผู้จัดการหน่วยเก็บข้อมูลอื่น ๆ ที่ใช้deleteวิธีการนี้ ( deleteวิธีการเป็นหนึ่งในstorageวิธีนามธรรม)

แทนที่deleteเมธอดของโมเดลด้วยวิธีที่อินสแตนซ์ลบ FileFields ก่อนที่จะลบตัวเอง:

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

มันใช้งานได้ง่ายสำหรับฉัน หากคุณต้องการตรวจสอบว่ามีไฟล์อยู่หรือไม่ก่อนที่จะลบคุณสามารถใช้storage.existsไฟล์. เช่นself.song.storage.exists(self.song.name)จะส่งคืนbooleanตัวแทนหากมีเพลงอยู่ ดังนั้นจะมีลักษณะดังนี้:

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

แก้ไข (เพิ่มเติม):

ตามที่@HeyManกล่าวไว้ด้วยการเรียกใช้โซลูชันนี้Song.objects.all().delete()จะไม่ลบไฟล์! นี้เกิดขึ้นเพราะSong.objects.all().delete()มีการเรียกใช้แบบสอบถามลบของผู้จัดการเริ่มต้น ดังนั้นหากคุณต้องการลบไฟล์ของโมเดลโดยใช้objectsเมธอดคุณต้องเขียนและใช้Custom Manager (เพียงเพื่อลบล้างคิวรีลบ):

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

และสำหรับการกำหนดCustomManagerให้กับโมเดลคุณต้องเริ่มต้นobjectsภายในโมเดลของคุณ:

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

ตอนนี้คุณสามารถใช้.delete()ในส่วนท้ายของobjectsแบบสอบถามย่อย ฉันเขียนแบบง่ายที่สุดCustomManagerแต่คุณสามารถทำได้ดีกว่าโดยส่งคืนบางอย่างเกี่ยวกับวัตถุที่คุณลบไปหรืออะไรก็ได้ที่คุณต้องการ


1
ใช่ฉันคิดว่าพวกเขาเพิ่มฟีเจอร์นั้นตั้งแต่ฉันโพสต์คำถาม
Marcos Aguayo

1
ยังคงลบไม่ได้เรียกว่า wenn เรียก Song.objects.all () ลบ () เช่นเดียวกับเมื่ออินสแตนซ์ถูกลบโดย on_delete = models.CASCADE
HeyMan

@HeyMan ฉันแก้ไขและแก้ไขโซลูชันของฉันตอนนี้ :)
Hamidreza

4

นี่คือแอพที่จะลบไฟล์เก่าทุกครั้งที่โมเดลถูกลบหรืออัปโหลดไฟล์ใหม่: 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 Strogonoff

ฉันขาดบางอย่างในรหัสเมื่อไฟล์เปลี่ยนหากคุณสร้างไฟล์ใหม่ทำให้เกิดข้อผิดพลาดเนื่องจากเป็นไฟล์ใหม่และไม่พบเส้นทาง ฉันแก้ไขโค้ดของฟังก์ชันและเพิ่มประโยค try / except และใช้ได้ดี

@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

ฉันไม่พบสิ่งนี้ - อาจเป็นข้อบกพร่องในโค้ดของฉันหรือมีอะไรเปลี่ยนแปลงใน Django ฉันขอแนะนำให้จับข้อยกเว้นเฉพาะในtry:บล็อกของคุณแม้ว่า ( AttributeErrorอาจ?)
Anton Strogonoff

ไม่ควรใช้ไลบรารีระบบปฏิบัติการเนื่องจากคุณจะพบปัญหาหากคุณย้ายข้อมูลไปยังที่เก็บข้อมูลอื่น (เช่น Amazon S3)
Igor Pomaranskiy

@IgorPomaranskiy จะเกิดอะไรขึ้นในสตอเรจเช่น Amazon S3 เมื่อคุณใช้ os.remove ??
Daniel GonzálezFernández

@ DanielGonzálezFernándezฉันเดาว่ามันจะล้มเหลว (มีข้อผิดพลาดบางอย่างเกี่ยวกับเส้นทางที่ไม่มีอยู่) นั่นเป็นเหตุผลที่ Django ใช้ abstractions สำหรับการจัดเก็บ
Igor Pomaranskiy

0

รหัสนี้จะทำงานทุกครั้งที่ฉันอัปโหลดภาพใหม่ (ช่องโลโก้) และตรวจสอบว่ามีโลโก้อยู่แล้วหรือไม่ถ้าเป็นเช่นนั้นให้ปิดและนำออกจากดิสก์ แน่นอนว่าขั้นตอนเดียวกันนี้สามารถทำได้ในฟังก์ชันตัวรับสัญญาณ หวังว่านี่จะช่วยได้

 #  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='')
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.