4

I have a Django model that saves filename as "uuid4().pdf". Where uuid4 generates a random uuid for each instance created. This file name is also stored on the amazon s3 server with the same name.

I am trying to add a custom disposition for filename that i upload to amazon s3, this is because i want to see a custom name whenever i download the file not the uuid one. At the same time, i want the files to stored on s3 with the uuid filename.

So, I am using django-storages with python 2.7. I have tried adding content_disposition in settings like this:

AWS_CONTENT_DISPOSITION = 'core.utils.s3.get_file_name'

where get_file_name() returns the filename.

I have also tried adding this to the settings:

AWS_HEADERS = {
'Content-Disposition': 'attachments; filename="%s"'% get_file_name(),

 }

no luck!

Do anyone of you know to implement this.

Yaman Ahlawat
  • 477
  • 6
  • 17

4 Answers4

5

Current version of S3Boto3Storage from django-storages supports AWS_S3_OBJECT_PARAMETERS global settings variable, which allows modify ContentDisposition too. But the problem is that it is applied as is to all objects that are uploaded to s3 and, moreover, affects all models working with the storage, which may turn to be not the expected result.

The following hack worked for me.

from storages.backends.s3boto3 import S3Boto3Storage

class DownloadableS3Boto3Storage(S3Boto3Storage):

    def _save_content(self, obj, content, parameters):
        """
        The method is called by the storage for every file being uploaded to S3.
        Below we take care of setting proper ContentDisposition header for
        the file.
        """
        filename = obj.key.split('/')[-1]
        parameters.update({'ContentDisposition': f'attachment; filename="{filename}"'})
        return super()._save_content(obj, content, parameters)

Here we override native save method of the storage object and make sure proper content disposition is set on each file. Of course, you need to feed this storage to the field you working on:

my_file_filed = models.FileField(upload_to='mypath', storage=DownloadableS3Boto3Storage())
abcdn
  • 1,397
  • 14
  • 15
4

In case someone finds this, like I did: none of the solutions mentioned on SO worked for me in Django 3.0.

Docstring of S3Boto3Storage suggests overriding S3Boto3Storage.get_object_parameters, however this method only receives name of the uploaded file, which at this point has been changed by upload_to and can differ from the original.

What worked is the following:

class S3Boto3CustomStorage(S3Boto3Storage):
    """Override some upload parameters, such as ContentDisposition header."""

    def _get_write_parameters(self, name, content):
        """Set ContentDisposition header using original file name.

        While docstring recomments overriding `get_object_parameters` for this purpose,
        `get_object_parameters` only gets a `name` which is not the original file name,
        but the result of `upload_to`.
        """
        params = super()._get_write_parameters(name, content)
        original_name = getattr(content, 'name', None)
        if original_name and name != original_name:
            content_disposition = f'attachment; filename="{original_name}"'
            params['ContentDisposition'] = content_disposition
        return params

and then using this storage in the file field, e.g.:


    file_field = models.FileField(
        upload_to=some_func,
        storage=S3Boto3CustomStorage(),
    )

Whatever solution you come up with, do not change file_field.storage.object_parameters directly (e.g. in model's save() as it's been suggested in a similar question), because this will change ContentDisposition header for subsequent file uploads of any field that uses the same storage. Which is not what you probably want.

railla
  • 201
  • 2
  • 6
2

I guess you are using S3BotoStorage from django-storages, so while uploading the file to S3, override the save() method of the model, and set the header there.

I am giving an example below:

class ModelName(models.Model):
    sthree = S3BotoStorage()
    def file_name(self,filename):
        ext = filename.split('.')[-1]
        name = "%s/%s.%s" % ("downloads", uuid.uuid4(), ext)
        return name
    upload_file = models.FileField(upload_to=file_name,storage = sthree)
    def save(self):
        self.upload_file.storage.headers = {'Content-Disposition': 'attachments; filename="%s"' %self.upload_file.name}
        super(ModelName, self).save()
Pang
  • 9,564
  • 146
  • 81
  • 122
Sibasish
  • 346
  • 3
  • 10
  • This could be adjusted to conditionally set the headers only if you are using `S3BotoStorage`. With that you can easily create tests without having to care about S3. That's why it takes my vote. – rafaelpivato Apr 29 '20 at 13:02
1

One way can be giving ResponseContentDisposition parameter to S3Boto3Storage.url() method. In this case you don't have to create a custom storage.

Example model:

class MyModel(models.Model):
    file = models.FileField(upload_to=generate_upload_path)
    original_filename = models.CharField(max_length=255)

Creating URL for your file:

# obj is instance of MyModel
url = obj.file.storage.url(
    obj.file.name,
    parameters={
        'ResponseContentDisposition': f'inline; filename={obj.original_filename}',
    },
)

If you want to force browser to download the file, replace inline with attachment.

If you are using non-ascii filenames, check how Django encodes filename for Content-Disposition header in FileResponse.

iqqmuT
  • 653
  • 6
  • 8
  • This is just what I was looking for, thank you. Make sure, if you're using something like CloudFront, that you update your cache policy to account for this query string. – nullable Apr 28 '22 at 08:31