Thursday, January 1, 2015

Simple example of a customized FileSystemStorage at Django 1.7

At this post it is published an example about how to implement your own FileSystemStorageClass and some extra methods which allow you to remove old images(files), once its value has changed and also when the instance is deleted, because these two behaviors seems like they should have been included at the normal workflow, of an ImageField (FileField), but they are not.

Setting out the initial approach

In first place you usually would like to develop a new model class whose goal is handling a type of file (in this case Images). I called my class Photo:

class Photo(models.Model):
    image = models.ImageField(location='Public/uploading')
    title = models.CharField(max_length=70, blank=False)
    alt = models.CharField(max_length=110, blank=True, null=True)
    description = models.TextField(null=True, blank=True)

As you see, at my first go I decided to store every file within the Public folder of the project itself because in my case I wanted to use these images a part of the html templating. Usually, glancing at  Django 1.7 manual it is recommend to you to use an absolute path to the media folder, out of project folder. You will see why ....

Problems showing up

Problem 1: Admin area

My first problem came after uploading the first file. It seems like everything is going to work "fine", apparently, till you click on the image link and it is loaded an url like that:
http://127.0.0.1:8000/admin/content/photo/6/Public/uploading/cartel_autoestima_Nuevo-1_l4w4IzB.png
And of course, nothing will load.

Problem 2: Rendering templates

Okay.... Problem 1 was annoying, we can carry on with this, deleting the removing the surplus from the url. Then the next challenge was using them in the templates. You have something like the following code.
<img src="/{{ photo.image }}"> 
As you have already realized, I have had to add and slash / before using the actual image url. It is another detail what tell you something is not done properly at the initial approach you implemented.

Problem 3: Endless amount of files

By now we have patched every "problem" we found in the way, and we don't feel happy how is done, because you know you are doing really dirty stuff to keep going ahead. Then once you try to face up to this one it forces you to change all done so far. Every file you have uploaded during all your testing process is still there, they never got deleted even after deleting the instance, and you have to clean it up manually because you are not going to fill up the server with useless files everywhere. 

Coding your customized FileSystemStorage

Due to all problem explained above we decide to redo everything and after some researches you find out that the best way to work them out is developing a customized FileSystemStorage class which will handle everything that were bothering you.
# Settings.py file
CUSTOM_STORAGE_FOLDER = '/Public/uploading'CUSTOM_STORAGE_FOLDER_ROOT = os.path.join(BASE_DIR, 'Public/uploading')
# models.py file
class MyStorage(FileSystemStorage):

        def url(self, name):
            if self.base_url is None:
                raise ValueError("This file is not accessible via a URL.")
            return os.path.join(self.base_url, filepath_to_uri(name))

fs = MyStorage(location=settings.CUSTOM_STORAGE_FOLDER_ROOT, base_url=settings.CUSTOM_STORAGE_FOLDER)


class Photo(models.Model):
    image = models.ImageField(storage=fs)
    title = models.CharField(max_length=70, blank=False)
    alt = models.CharField(max_length=110, blank=True, null=True)
    description = models.TextField(null=True, blank=True)

    def delete(self, *args, **kwargs):
        self.image.delete()
        super(Photo, self).delete(*args, **kwargs)

    def save(self, *args, **kwargs):
        if self.pk is not None:
            try:
                this = Photo.objects.get(id=self.id)
                if this.image != self.image:
                    this.image.delete(save=False)
            except OSError as e:
                pass
        super(Photo, self).save(*args, **kwargs)
Lets explain a bit what I do in here. 
  • Implement a new class called MyStorage which extends the FileSystemStorage, and we just override the def url method so you build up the image url you will need to use to render the files in the templates, and it is also automatically used at the admin panel too.
  • Instance of MyStorage using location and base_url parameters to define full storage folder path and rendering url respectively, and at the ImageField declaration we use it as storage object.
  • At last, we have overrided the methods delete and save to include the removing of old images, after changing the image value and after deleting the instanced object.

I hope this post makes your coding bit easier
Thanks for reading it ;)