Replacing files in a Django ImageField or FieldField
September 24th, 2008
Django provides a lot of really useful tools to simplify the development process and let you focus only on the important bits. The FileField and ImageField (a subclass of FileField) are good examples of that letting you simply tell Django that your model will have a file or image and letting it take care of the issues of uploading, storing, and all that. In the past, that's really been enough. It will even automatically delete the file/image when you delete its parent model object. One thing it doesn't do, however (and this has been the topic of much debate), is delete a previously uploaded file/image when you upload a new one for the same model. What I mean by this is if you have a model with a FileField defined and use it to upload some file associated with an object. If you then later decide that you want a different file associated with that object and upload it, it doesn't delete the original file and instead leaves it in place and only changes the path stored in the database to point to the new file. Depending on the nature and traffic your website gets, this can lead to massive amounts of storage being wasted on orphaned files (assuming you don't want to keep those old files, of course). I toyed with a couple different approaches to this, including the possibility of subclassing the FileField to try and add that functionalty directly to the field. While this would probably work, I instead opted for a less generalized method: overriding the save() method of the model to take care of this:
def save(self, force_insert=False, force_update=False):
try:
old_obj = ModelName.objects.get(pk=self.pk)
if old_obj.image.path != self.image.path:
path = old_obj.image.path
default_storage.delete(path)
except:
pass
super(ModelName, self).save(force_insert, force_update)
This work perfectly, though it has the disadvantage of being specific to a particular model, which violates the DRY principle (assuming you're going to use it on more than one model). Fortunately there's a simple way to solve this problem too: subclassing models.Model and then instead of having all your models subclass models.Model directly, have them subclass your own version of it instead. In that case you'll probably want to have it work generically on all ImageFields and/or FileFields rather than having to name them all specifically. This isn't too hard, and you can build up a list of all the ImageFields for a particular model like this (taken from the sorl-thumnail project):
for field in model._meta.fields:
if isinstance(field, models.ImageField):
if field.upload_to.find("%") == -1:
paths = paths.union((field.upload_to,))
[Edit: There was a bug in my code that I've corrected. Details are below in the comment by Comete.]
[Edit2: Fixed another problem in the code with variable names. Thanks, again Comete!]
Comete wrote:
on Thursday, October 30th 2008 at 4:50 p.m.
hi,
good idea but the only problem with this code is when you want to modify another data in a text field, by example, and save your modification. Then it will delete the content of the ImageField even if you didn't want to change it...
Josh wrote:
on Thursday, October 30th 2008 at 4:53 p.m.
Oops, I fixed that problem in my own code but guess I never did here. Thanks for the catch! I'll change it!
Comete wrote:
on Friday, October 31st 2008 at 5:26 a.m.
Why using old_obj.image_large.path on line 4 and old_obj.image.path on line 5 ?
Maybe i don't understand something...
Josh wrote:
on Friday, October 31st 2008 at 8:27 a.m.
No good reason. image_large is the name of a variable I'm using in the class I originally wrote the method for, and I guess I forgot to change it to image for this.
Comete wrote:
on Friday, October 31st 2008 at 10:24 a.m.
Thanks for this article ! Exactly what i was looking for. I use it now and I also added a checkbox to delete the image.
Josh wrote:
on Friday, October 31st 2008 at 10:52 a.m.
Glad to hear it! The delete checkbox is useful, I use it in a couple places as well.