Amazon S3 Change file download name

RestAmazon S3Amazon Web-Services

Rest Problem Overview


I have files stored on S3 with a GUID as the key name.

I am using a pre signed URL to download as per S3 REST API

I store the original file name in my own Database. When a user clicks to download a file from my web application I want to return their original file name, but currently all they get is a GUID. How can I achieve this?

My web app is in salesforce so I do not have much control to do response.redirects all download the file to the web server then rename it due to governor limitations.

Is there some HTML redirect, meta refresh, Javascript I can use? Is there some way to change the download file name for S3 (the only thing I can think of is coping the object to a new name, downloading it, then deleting it).

I want to avoid creating a bucket per user as we will have a lot of users and still no guarantee each file with in each bucket will have a unique name

Any other solutions?

Rest Solutions


Solution 1 - Rest

I guess your cross posted this questions to Amazon S3 forum, but for the sake of others I'd like to post the answer here:

If there is only ever one "user filename" for each S3 object, then you can set the Content-Disposition header on your s3 file to set the downloading filename:

Content-Disposition: attachment; filename="foo.bar"

For the sake of fairness I'd like to mention that it was not me to provide the right answer on Amazon forum and all credits should go to Colin Rhodes ;-)

Solution 2 - Rest

While the accepted answer is correct I find it very abstract and hard to utilize.

Here is a piece of node.js code that solves the problem stated. I advise to execute it as the AWS Lambda to generate pre-signed Url.

var AWS = require('aws-sdk');
var s3 = new AWS.S3({
    signatureVersion: 'v4'
});
const s3Url = process.env.BUCKET;

module.exports.main = (event, context, callback) => {
var s3key = event.s3key
var originalFilename = event.originalFilename

var url = s3.getSignedUrl('getObject', {
        Bucket: s3Url,
        Key: s3key,
        Expires: 600,
        ResponseContentDisposition: 'attachment; filename ="' + originalFilename + '"'
    });

[... rest of Lambda stuff...]

}

Please, take note of ResponseContentDisposition attribute of params object passed into s3.getSignedUrl function.

More information under getObject function doc at http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getObject-property

Solution 3 - Rest

In early January 2011 S3 added request header overrides. This functionality allows you to 'dynamically' alter the Content-Disposition header for individual requests.

See the S3 documentation on getting objects for more details.

Solution 4 - Rest

With C# using AWSSDK,

GetPreSignedUrlRequest request = new GetPreSignedUrlRequest
{
    BucketName = BucketName,
    Key = Key,
    Expires = DateTime.Now.AddMinutes(25) 
};

request.ResponseHeaderOverrides.ContentDisposition = $"attachment; filename={FileName}";

var url = s3Client.GetPreSignedURL(request);

Solution 5 - Rest

For Java AWS SDK below Code Snippet should do the job:

GeneratePresignedUrlRequest generatePresignedUrlRequest = 
                new GeneratePresignedUrlRequest(s3Bucket, objectKey)
                .withMethod(HttpMethod.GET)
                .withExpiration(getExpiration());
        
ResponseHeaderOverrides responseHeaders = new ResponseHeaderOverrides();
responseHeaders.setContentDisposition("attachment; filename =\"" + fileName + "\"");
		
generatePresignedUrlRequest.setResponseHeaders(responseHeaders);

Solution 6 - Rest

It looks like :response_content_disposition is undocumented in the presigned_url method. This is what worked for me

    signer = Aws::S3::Presigner.new
    signer.presigned_url(:get_object, bucket: @bucket, key: filename, 
    response_content_disposition: "attachment; filename =#{new_name}")

Solution 7 - Rest

Using python and boto v2:

    conn = boto.connect_s3(
        AWS_ACCESS_KEY_ID,
        AWS_SECRET_ACCESS_KEY,
        host=settings.AWS_S3_HOST,
    )
    b = conn.get_bucket(BUCKET_NAME)
    key = b.get_key(path)
    url = key.generate_url(
        expires_in=60 * 60 * 10,  # expiry time is in seconds
        response_headers={
            "response-content-disposition": "attachment; filename=foo.bar"
        },
    )

Solution 8 - Rest

I have the same issue, I solved it by set http header "content-disposition" while submit the file to S3, the SDK version is AWS SDK for PHP 3.x. here is the doc http://docs.amazonaws.cn/en_us/aws-sdk-php/latest/api-s3-2006-03-01.html#putobject

a piece of my code

    public function __construct($config) 
    {
        $this->handle = new S3Client([
            'credentials' => array(
                'key' => $config['key'],
                'secret' => $config['secret'],
            ),
            ...
        ]);

        ...
    }

    public function putObject($bucket, $object_name, $source_file, $content_type = false, $acl = 'public-read', $filename = '')
    {
        try {
            $params = [
                'Bucket'      => $bucket,
                'Key'         => $object_name,
                'SourceFile'  => $source_file,
                'ACL'         => $acl,
            ];

            if ($content_type) $params['ContentType'] = $content_type;
            if ($filename) $params['ContentDisposition'] = 'attachment; filename="' . $filename . '"';

            $result = $this->handle->putObject($params);

            ...
        }
        catch(Exception $e)
        {
            ...
        }
    }

Solution 9 - Rest

I spent a few hours to find this solution.

const { CloudFront } = require("aws-sdk");
const url = require("url");

const generateSingedCloudfrontUrl = (path) => {
  const cloudfrontAccessKeyId = process.env.CF_ACCESS_KEY;
  const cloudFrontPrivateKey = process.env.CF_PRIVATE_KEY;
  const formattedKey = `${"-----BEGIN RSA PRIVATE KEY-----"}\n${cloudFrontPrivateKey}\n${"-----END RSA PRIVATE KEY-----"}`;
  const signer = new CloudFront.Signer(cloudfrontAccessKeyId, formattedKey);
  //  12 hours
  const EXPIRY_TIME = 43200000;

  const domain = process.env.CF_DOMAIN;
  const signedUrl = signer.getSignedUrl({
    url: url.format(`https://${domain}/${path}`),
    expires: Math.floor((Date.now() + EXPIRY_TIME) / 1000),
  });
  return signedUrl;
};

const fileName = "myFile.png";
  const result = generateSingedCloudfrontUrl(
    `originals/orgs/originals/MSP/1539087e-02b7-414f-abc8-3542ee0c8420/1644588362499/Screenshot from 2022-02-09 16-29-04..png?response-content-disposition=${encodeURIComponent(
      `attachment; filename=${fileName}`
    )
});

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDaveoView Question on Stackoverflow
Solution 1 - RestcloudberrymanView Answer on Stackoverflow
Solution 2 - RestPawelView Answer on Stackoverflow
Solution 3 - RestUriah CarpenterView Answer on Stackoverflow
Solution 4 - ResthkutluayView Answer on Stackoverflow
Solution 5 - RestAbhishek BasakView Answer on Stackoverflow
Solution 6 - RestChris McdonaldView Answer on Stackoverflow
Solution 7 - RestUdiView Answer on Stackoverflow
Solution 8 - RestYugoAmarylView Answer on Stackoverflow
Solution 9 - RestJha NiteshView Answer on Stackoverflow