How to Dynamically Select Storage in Django FileField

Hitesh Garg
3 min readApr 25, 2020

Django provides a FileField, which takes a Storage class instance and perform actions of a file accordingly. But what if someone wants to switch that Storage class instance dynamically according to business logic? For eg, if you want to upload a file to Amazon S3 and you want to set ACL of that file based on user input. So, If someone chooses ACL type as public then the file should be accessible to everyone otherwise it should be private and the user should be able to update the ACL type in the future as well. There are many other cases like that where you would want to change the storage class dynamically. In this tutorial, we will be covering how to implement the above example but you can apply the same technique for any of your use cases.

Let’s create our database model first, which will store the image file

from django.db import models
from .fields import DynamicStorageFileField
class Image(models.Model):
ACL_TYPES = (
('public', 'Public'),
('private', 'Private'),
)
name = models.CharField(max_length=250, blank=True)
image = DynamicStorageFileField(
upload_to='media',
)
acl_type = models.CharField(max_length=100, choices=ACL_TYPES)
def __str__(self):
return self.name

In the above code, you can notice that we have used DynamicStorageFileField instead of traditional models.FileField. But before we take a look at the code of DynamicStorageFileField, let’s create PublicImageStorage and PrivateImageStorage classes that will hold our public and private images respectively. We will be using django-storages to handle S3 file uploads, which also provides a collection of many custom storage backends for Django.

from storages.backends.s3boto3 import S3Boto3Storageclass UpdateACLMixin:
def update_acl(self, name):
name = self._normalize_name(self._clean_name(name))
self.bucket.Object(
self._encode_name(name)
).Acl().put(ACL=self.default_acl)
class PublicImageStorage(UpdateACLMixin, S3Boto3Storage):
default_acl = "public-read"
file_overwrite = False
class PrivateImageStorage(UpdateACLMixin ,S3Boto3Storage):
default_acl = 'private'
file_overwrite = False
custom_domain = False

In the above code, We have created a UpdateACLMixin class. Which holds the method to update the ACL type as public or private based on storage’s default ACL. And We have just created two storage classes and inherit them from the UpdateACLMixin class. Now, let’s take a look at the code of DynamicStorageFileField.

from django.db import models
from django.db.models.fields.files import FieldFile
class DynamicStorageFieldFile(FieldFile):
def __init__(self, instance, field, name):
super(DynamicStorageFieldFile, self).__init__(
instance, field, name
)
if instance.acl_type == "private":
self.storage = PrivateImageStorage()
else:
self.storage = PublicImageStorage()

def update_acl(self):
if not self:
return
# Only close the file if it's already open, which we know by
# the presence of self._file
if hasattr(self, '_file'):
self.close()
# This update_acl method we have already defined in
# UpdateACLMixin class
self.storage.update_acl(self.name)

class DynamicStorageFileField(models.FileField):
attr_class = DynamicStorageFieldFile
def pre_save(self, model_instance, add):
if model_instance.acl_type == "private":
storage = PrivateImageStorage()
else:
storage = PublicImageStorage()
self.storage =storage
self.image.storage = storage
file = super(DynamicStorageFileField, self
).pre_save(model_instance, add)

if file and file._committed:
# This update_acl method we have already defined
# in DynamicStorageFieldFile class above.
file.update_acl()
return file

In the above code, We have created two classes, DynamicStorageFieldFile and DynamicStorageFileField. Our main DynamicStorageFileField class is a wrapper around models.FileField. We have overwritten attr_class attribute of that default FileField class and modified pre_save method according to our requirements.

Now, our code is ready to be used. You can easily upload a file as public/private on S3 and change its ACL type. But there a problem though, current code will try to update the ACL every time we will update the Image model instance. To overcome this, We can just simply add a pre_save signal to the Image model and determine whether the acl_type field has changed on not. Based on that, we can set a custom attribute to model_instance and use that to determine whether we should call file.update_acl() or not. I am not covering this in this tutorial but if anyone wants me to provide the implementation code of that solution as well then please let me know. The above technique can be used with any storage class which uses Django’s default Storage class.

So, that’s it. Let me know if you face any difficulties to implement the above code or in case you have any questions.

Follow me on Twitter

--

--

Hitesh Garg

Software Developer || Google Summer of Code 2017 participant with Open States