Replacing a Django image doesn't delete original
Asked Answered
H

8

26

In Django, if you have a ImageFile in a model, deleting will remove the associated file from disk as well as removing the record from the database.

Shouldn't replacing an image also remove the unneeded file from disk? Instead, I see that it keeps the original and adds the replacement.

Now deleting the object won't delete the original file only the replacement.

Are there any good strategies to doing this? I don't want to have a bunch of orphan files if my users replace their images frequently.

Hypabyssal answered 9/12, 2010 at 2:0 Comment(2)
have you tried using a save method on the model to check if file is being updated and delete the old file if necessary?Universe
I have a similar problem and have added a question for it. You might find what you're looking for there in the following days...Cheviot
O
34

The best strategy I've found is to make a custom save method in the model:

class Photo(models.Model):

    image = ImageField(...) # works with FileField also

    def save(self, *args, **kwargs):
        # delete old file when replacing by updating the file
        try:
            this = Photo.objects.get(id=self.id)
            if this.image != self.image:
                this.image.delete(save=False)
        except: pass # when new photo then we do nothing, normal case          
        super(Photo, self).save(*args, **kwargs)

And beware, as with the updating which doesn't delete the back end file, deleting an instance model (here Photo) will not delete the back-end file, not in Django 1.3 anyway, you'll have to add more custom code to do that (or regularly do some dirty cron job).

Finally test all your update/delete cases with your ForeignKey, ManytoMany and others relations to check if the back-end files are correctly deleted. Believe only what you test.

Oligochaete answered 1/12, 2011 at 13:37 Comment(4)
After almost 3 years used in 2 websites I can confirm this method is production ready and with zero problems. I now use this method in a third website with Django 1.6.2. and it's still working great.Oligochaete
used it in save_model() and delete_model() functions of ModelAdmin class in django 1.9 and it's working like a charmStellate
I see that the file name still gets appended with a hash code to avoid conflicts with the original file name. This could be a drawback? like if the filename was specified as username.jpg, first time the storage save as username.jpg, but subsequently it updates to username_lksjdflaj.jpg. That is, the file name conflict resolution still happend...Dunlap
today I have tested your solution on Django 3.0.5, it works. Thank you!Varro
C
16

Shouldn't replacing an image also remove the unneeded file from disk?

In the olden days, FileField was eager to clean up orphaned files. But that changed in Django 1.2:

In earlier Django versions, when a model instance containing a FileField was deleted, FileField took it upon itself to also delete the file from the backend storage. This opened the door to several potentially serious data-loss scenarios, including rolled-back transactions and fields on different models referencing the same file. In Django 1.2.5, FileField will never delete files from the backend storage.

Claypool answered 4/2, 2013 at 4:8 Comment(0)
F
9

The code in the following working example will, upon uploading an image in an ImageField, detect if a file with the same name exists, and in that case, delete that file before storing the new one.

It could easily be modified so that it deletes the old file regardless of the filename. But that's not what I wanted in my project.

Add the following class:

from django.core.files.storage import FileSystemStorage
class OverwriteStorage(FileSystemStorage):
    def _save(self, name, content):
        if self.exists(name):
            self.delete(name)
        return super(OverwriteStorage, self)._save(name, content)

    def get_available_name(self, name):
        return name

And use it with ImageField like so:

class MyModel(models.Model):
    myfield = models.ImageField(
        'description of purpose',
        upload_to='folder_name',
        storage=OverwriteStorage(),  ### using OverwriteStorage here
        max_length=500,
        null=True,
        blank=True,
        height_field='height',
        width_field='width'
    )
    height = models.IntegerField(blank=True, null=True)
    width = models.IntegerField(blank=True, null=True)
Fermentative answered 4/7, 2012 at 9:14 Comment(3)
@Philip007 Um it was a long time ago I wrote this.. but I'm pretty sure it's the file name?Fermentative
what does max_length do on image field??Henley
@dietbacon From Django documentation: "FileField instances are created in your database as varchar columns with a default max length of 100 characters. As with other fields, you can change the maximum length using the max_length argument."Fermentative
S
2

If you don't use transactions or you don't afraid of loosing files on transaction rollback, you can use django-cleanup

Shutt answered 12/9, 2012 at 5:47 Comment(0)
C
1

There have been a number of tickets regarding this issue though it is likely this will not make it into the core. The most comprehensive is http://code.djangoproject.com/ticket/11663. The patches and ticket comments can give you some direction if you are looking for a solution.

You can also consider using a different StorageBackend such as the Overwrite File Storage System given by Django snippet 976. http://djangosnippets.org/snippets/976/. You can change your default storage to this backend or you can override it on each FileField/ImageField declaration.

Chuck answered 17/1, 2011 at 19:4 Comment(2)
I think there is a problem with snippet 976. Consider a UserProfile model that has a profile image field using this OverwriteStorage backend with upload_to="%Y/%m/%d". Two users on the same day upload a profile picture with the name "me.jpg". The first upload will go in 2012/02/11/me.jpg. The second upload will delete and replace this image and cause the two UserProfile image fields to reference the same image.Awning
The expected behavior of the OverwriteStorage is to replace the file. That means ensuring the uniqueness of the name falls on the site developer rather than the backend.Chuck
A
1

Here is a code that can work with or without upload_to=... or blank=True, and when the submitted file has the same name as the old one.

(py3 syntax, tested on Django 1.7)

class Attachment(models.Model):

    document = models.FileField(...)  # or ImageField

    def delete(self, *args, **kwargs):
        self.document.delete(save=False)
        super().delete(*args, **kwargs)

    def save(self, *args, **kwargs):
        if self.pk:
            old = self.__class__._default_manager.get(pk=self.pk)
            if old.document.name and (not self.document._committed or not self.document.name):
                old.document.delete(save=False)
        super().save(*args, **kwargs)

Remember that this kind of solution is only applicable if you are in a non transactional context (no rollback, because the file is definitively lost)

Aitch answered 28/11, 2014 at 11:3 Comment(1)
for those who are still using python 2.x use super(Attachment,self) instead of super()Duplessis
O
0

I used a simple method with popen, so when i save my Info model i delete the former file before linking to the new:

import os

try:
    os.popen("rm %s" % str(info.photo.path))
except:
    #deal with error
    pass
info.photo = nd['photo']
Ordinand answered 17/1, 2011 at 15:18 Comment(0)
L
0

I save the original file and if it has changed - delete it.

class Document(models.Model):
    document = FileField()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._document = self.document

    def save(self, *args, **kwargs):
        if self.document != self._document:
            self._document.delete()
            super().save(*args, **kwargs)
Litta answered 1/11, 2018 at 11:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.