How do I get Django Admin to delete files when I remove an object from the database/model?
Asked Answered
Z

13

95

I am using 1.2.5 with a standard ImageField and using the built-in storage backend. Files upload fine but when I remove an entry from admin the actual file on the server does not delete.

Zenia answered 21/3, 2011 at 1:19 Comment(2)
Hm, actually it should. Check file permissions on your upload folder (change to 0777).Lustral
Django removed the automatic deletion feature (for Googlers who see the above comment).Mulish
C
110

You can receive the pre_delete or post_delete signal (see @toto_tico's comment below) and call the delete() method on the FileField object, thus (in 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)
Carraway answered 14/1, 2013 at 1:11 Comment(7)
Be sure to add a check if instance.file field is not empty or it can (at least try) to delete the whole MEDIA_ROOT directory. This applies even to ImageField(null=False) fields.Katti
Thanks. In general, I would recommend to use the post_delete signal because it is safer in the case the delete fail for any reason. Then neither the model, neither the file would be deleted keeping the data consistent. Please correct me if my understanding of post_delete and pre_delete signals is wrong.Dashtikavir
Note that this does not delete the old file if you replace the file on a model instanceMulish
This does not work for me in Django 1.8 outside admin. Is there a new way to do it?Burlburlap
It does work in Django 1.11. You should use pre_delete signal, ** post_delete** won't work.Slovene
@jonalv Connect the signal in Django 1.9 to make it work: request_finished.connect(mymodel_delete, sender=None, weak=True, dispatch_uid="mymodel_delete"). Then the signature of mymodel_delete should be: @receiver(post_delete, sender=Resource) def mymodel_delete(sender, **kwargs): instance = kwargs.get('instance')Misesteem
I just user it and it works in Django 2.1 with python 3.7Accelerometer
C
59

Try django-cleanup

pip install django-cleanup

settings.py

INSTALLED_APPS = (
    ...
    'django_cleanup.apps.CleanupConfig',
)
Carver answered 17/3, 2015 at 17:21 Comment(3)
After limited testing, I can confirm that this package still works for Django 1.10.Tribunate
Nice. Works for me on Django 2.0. I'm also using S3 as my storage backend (django-storages.readthedocs.io/en/latest/backends/…) and it's happily deleting files from S3.Repercussion
Working on Django 4.xRoband
C
35

Django 1.5 solution: I use post_delete for various reasons that are internal to my app.

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)

I stuck this at the bottom of the models.py file.

the original_image field is the ImageField in my Photo model.

Calumniate answered 8/5, 2013 at 14:15 Comment(4)
For anyone using Amazon S3 as a storage backend (via django-storages), this particular answer won't work. You'll get a NotImplementedError: This backend doesn't support absolute paths. You can easily fix this by passing the file field's name to storage.delete() instead of the file field's path. For example, replace the last two lines of this answer with storage, name = photo.original_image.storage, photo.original_image.name then storage.delete(name).Neuroticism
@Sean +1, I'm using that adjustment in 1.7 to delete thumbnails generated by django-imagekit on S3 via django-storages. docs.djangoproject.com/en/dev/ref/files/storage/… . Note: If you're simply using an ImageField (or FileField) you can use mymodel.myimagefield.delete(save=False) instead. docs.djangoproject.com/en/dev/ref/files/file/…Leflore
@Leflore Can you use mymodel.myimagefield.delete(save=False) on post_delete? In other words, I can see that I can delete the file, but can you delete the file when a model that has the imagefield gets deleted?Enisle
@Enisle Yes you can, it works (I'm not sure why though). In post_delete you do instance.myimagefield.delete(save=False), note the use of instance.Leflore
M
18

This code runs well on Django 1.4 also with the Admin panel.

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)

It's important to get the storage and the path before delete the model or the latter will persist void also if deleted.

Mistrust answered 4/7, 2012 at 17:28 Comment(2)
This doesn't work for me (Django 1.5) and the Django 1.3 CHANGELOG states: "In Django 1.3, when a model is deleted the FileField’s delete() method won’t be called. If you need cleanup of orphaned files, you’ll need to handle it yourself (for instance, with a custom management command that can be run manually or scheduled to run periodically via e.g. cron)."Carraway
This solution is wrong! delete is not always called when a row is deleted, you must use signals.Oshiro
T
17

You need to remove the actual file on both delete and 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)
Transistor answered 18/9, 2015 at 1:30 Comment(1)
Great solution!Deguzman
B
6

You may consider using a pre_delete or post_delete signal:

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

Of course, the same reasons that FileField automatic deletion was removed also apply here. If you delete a file that is referenced somewhere else you will have problems.

In my case this seemed appropriate because I had a dedicated File model to manage all of my files.

Note: For some reason post_delete doesn't seem to work right. The file got deleted, but the database record stayed, which is completely the opposite of what I would expect, even under error conditions. pre_delete works fine though.

Borchert answered 8/11, 2011 at 16:50 Comment(1)
probably post_delete won't work, because file_field.delete() by default saves model to db, try file_field.delete(False) docs.djangoproject.com/en/1.3/ref/models/fields/…Sublimate
G
3

Maybe it's a little late. But the easiest way for me is to use a post_save signal. Just to remember that signals are excecuted even during a QuerySet delete process, but the [model].delete() method is not excecuted during the QuerySet delete process, so it's not the best option to override it.

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

import os

def delete_image_slide(sender, **kwargs):
    slide = kwargs.get('instance')
    try:
        os.remove(slide.image.path)
    except:
        pass
Gigantic answered 16/8, 2013 at 2:26 Comment(0)
L
1

This functionality will be removed in Django 1.3 so I wouldn't rely on it.

You could override the delete method of the model in question to delete the file before removing the entry from the database completely.

Edit:

Here is a quick example.

class MyModel(models.Model):

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

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

        super(MyModel, self).delete(*args, **kwargs)
Limburger answered 21/3, 2011 at 2:54 Comment(2)
Do you have an example of how to use that in a model in order to delete the file? I'm looking at the docs and see examples of how to remove the object from the database but do not see any implementations on file deletion.Zenia
This method is wrong because it wont work for bulk delete (like admin's 'Delete selected' feature). For example MyModel.objects.all()[0].delete() will delete the file while MyModel.objects.all().delete() will not. Use signals.Katti
P
1

Using the post_delete is for sure the right way to go. Sometimes though things can go wrong, and files don't get deleted. There is of course the case that you have a bunch of old files that weren't deleted before post_delete was used. I created a function that deletes files for objects based on if the file the object references does not exist then delete object, if the file does not have an object, then also delete, also it can delete based on an "active" flag for an object.. Something I added to most of my models. You have to pass it the objects you want to check, the path to the objects files, the file field and a flag to delete inactive objects:

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

This is how you can use this:

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
Petrosal answered 5/6, 2014 at 23:22 Comment(0)
T
0

make sure you write "self" before the file. so example above should be

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

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

I've forgotten the "self" before my file and that didn't work as it was looking in the global namespace.

Tabb answered 27/3, 2013 at 17:15 Comment(0)
A
0

If you already have number of unused files in your project and want to delete them, you can use django utility django-unused-media

Adena answered 11/1, 2017 at 18:50 Comment(0)
N
0

Django 2.x Solution:

There's no need to install any packages! It's very easy to handle in Django 2. I've tried following solution using Django 2 and SFTP Storage (however I think it would work with any storages)

First write a Custom Manager. So if you want to be able to delete files of a model by using objects methods, you must write and use a [Custom Manager][3] (for overriding delete() method of objects):

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

Now you must delete image before deleting deleting the model itself and for assigning the CustomManager to the model, you must initial objects inside your model:

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()
Nip answered 25/10, 2019 at 11:14 Comment(0)
F
-1

I may have a special case since I am using the upload_to option on my file field with dynamic directory names but the solution I found was to use os.rmdir.

In models:

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)
Fifteenth answered 17/2, 2013 at 23:1 Comment(4)
This is a very bad idea. Not only will you remove an entire directory vs. a single file (potentially affecting other files), you will do so even if the actual object deletion fails.Poop
Its not a bad idea if you were working on the problem I had ;) As I mentioned I had a unique use case where the model being deleted was a parent model. Children wrote files to the parent folder and so if you deleted the parent the desired behavior was that all files in the folder were deleted. Good point on the order of operations though. That didn't occur to me at the time.Fifteenth
I would still prefer to remove the individual child files when a child is deleted; then if you need to you can remove the parent directory when it is empty.Poop
That makes sense as you are taking out child objects but if the parent object is destroyed, stepping through the children one at a time seems tedious and unnecessary. Regardless, I see now that the answer I gave wasn't specific enough to the OP question. Thank you for the comments, you made me think about using a less blunt instrument going forward.Fifteenth

© 2022 - 2024 — McMap. All rights reserved.