There is a specific way to upload files to Amazon S3. Unlike the traditional client-server approach, the client does not need to send any actual files to the server. Instead, the client asks the server for a pre-signed URL to an Amazon S3 bucket and then uploads the files directly to S3. If the client wants to upload multiple files, it needs to get a separate pre-signed URL for each and every file.
Visit the AWS CLI web page and follow the installation instructions for our operating system.
The AWS CLI is currently available on:
In order to connect to our AWS account via AWS CLI, we need to make our AWS access keys available:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
We can configure them as an environment variables. Alternatively, we can create a credentials
configuration file under our home directory:
~
└─.aws
└─credentials
The credentials
file contents:
[default]
AWS_ACCESS_KEY_ID=<your_access_key>
AWS_SECRET_ACCESS_KEY=<your_secret_access_key>
From shell, run the following command:
$ aws S3 mb s3://MY_BUCKET
Note that bucket names are globally shared on AWS so we need to choose a unique name for our bucket.
In order to generate upload URLs for S3 bucket, the server needs to assume a role with s3:PutObject
permissions. If the server needs to read the uploaded files from S3 bucket, s3:GetObject
permissions are also required.
Create a role trust policy JSON file (role-policy.json
) with the following contents:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Create a new role:
$ aws iam create-role --role-name ec2-s3-uploader --assume-role-policy-document file://role-policy.json
You can associate only one IAM instance profile with an instance.
List the EC2 instances and their states:
$ aws ec2 describe-instances --query "Reservations[*].Instances[*].[InstanceId,KeyName,State.Name,Tags[0].Value,PublicIpAddress]" --output table
Associates an IAM instance profile with a running EC2 instance by specifying the IAM instance profile ARN or the IAM instance profile name.
$ aws ec2 associate-iam-instance-profile --instance-id i-052baf6ba198a8e8a --iam-instance-profile Name="ec2-s3-uploader"
Create an access policy JSON file (s3-read-write-policy.json
) with the following contents:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::MY_BUCKET/*"
]
}
]
}
Add the access policy to the role:
$ aws iam put-role-policy --role-name ec2-s3-uploader --policy-name s3-read-write-policy-json --policy-document file://s3-read-write-policy.json
List all role policies:
$ aws iam list-role-policies --role-name ec2-s3-uploader
You should get a list of all the policies attached to a role:
{
"PolicyNames": [
"s3-read-write-policy-json"
]
}
Delete a role policy:
$ aws iam delete-role-policy --role-name ec2-s3-uploader --policy-name s3-read-write-policy-json
You can now fix the policy and re-attach it to the role, similarly to how we did it before.
The server generates pre-signed URLs to our S3 bucket. These URLs are sent to the client which in turn uses them to upload files directly to the S3 bucket. Uploaded files can be retrieved by the server for internal processing.
Install the AWS SDK node module:
$ npm i --save aws-sdk
Use the following code to generate a pre-signed upload URL with file size limit of 5 MB and expiration time of 10 minutes.
const aws = require("aws-sdk");
const { v4: uuidv4 } = require("uuid");
/**
* Generates a pre-signed upload URL with limitation on uploaded file size.
* This URL should be invoked via HTTP POST.
*
* @param {String} userId The id of the user who requested the upload URL.
* @param {String} filename The original name of the uploaded file.
* @return {Object} A pre-signed upload URL.
*/
function getUploadUrl(userId, filename) {
const params = {
"Bucket": MY_BUCKET,
"Fields": {
"Key": uuidv4(),
"x-amz-meta-userid": userId,
"x-amz-meta-filename": filename,
"success_action_status": "201"
},
"Conditions": [
["content-length-range", 1024, 5242880] // 1KB - 5MB
],
"Expires": 600
};
const s3 = new aws.S3({ "signatureVersion": "v4" });
return s3.createPresignedPost(params);
}
The response should look similar to this:
{
"url": "https://s3.amazonaws.com/MY_BUCKET",
"fields": {
"Key": "084deb78-8a6a-4c7b-84e9-7cc5c8bcf282",
"x-amz-meta-filename": "MY_FILE.PNG",
"success_action_status": "201",
"bucket": "MY_BUCKET",
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
"X-Amz-Credential": "ASIAV275AUXFJW5OA24C/20210427/us-east-1/s3/aws4_request",
"X-Amz-Date": "20210427T213411Z",
"X-Amz-Security-Token": "IQoJb3JpZ2luX2VjENb//////////wEaCXVzLWVhc3QtMSJHMEUCICN2jVmjNG8Fm+5XUQ09DzCnF/qpcwqACJGeYLmjTul5AiEAzNeHn3Q5jgnEpoKBxQ6/CvZCGdbRVOXyaXXop4ZduPIq0QEITxAAGgw0MDE1NzMxOTMxNjIiDITw6H0UYf/HTP6H8yquAfm2o500uvjiwksngr++aTcx3ws/FNQmgMDvy9X4jTWkXtpaS1Hz86hgtF8JarQD0A0y5yMdPjTbCJRlA/04cJC77pKdFfRVDFQ482PXhSNDzmq9cPL14VaFdkyytgSN5KPeIhZxSFZjrxp+Znkwv7LdQ2ncDrq8kGhwjt18y9wB16nkvDja5OOH53l/SoTWy7kh/w7cTQLmth7voRjx9Z371JYb4xbjrHqnNcr5EjDShqKEBjrgAe8txXUh1yfKcqqfszdvuWwrwLVbO0WzaodJu+ju3sg1sCG1lbYEHfOWR3WtdcippARySAwe3kM2mzdkyzg1CvFgFz2R1hub0Uw28f7ykjh1Hj4d0yU6e9KfGoxpwqWAPQgYdPH7OR6omlqqFfxa7wZ8ugVWXV9SSE1dh8daJP7az/nB2qxLa9DAX73rvI5sLmUqwnnZB5Htse85NGhCMCVI06fpe7Jpy3k8p2m8SUJmLcyhFgTwhbDViHrGdvFaEiU/ssAdCByhtbLCeLiW0BPAioUDTsZaiHDwoj3HZudj",
"Policy": "eyJleHBpcmF0aW9uIjoiMjAyMS0wNC0yN1QyMTozNToxMVoiLCJjb25kaXRpb25zIjpbWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsMSw1MjQyODgwXSx7IktleSI6IjA4NGRlYjc4LThhNmEtNGM3Yi04NGU5LTdjYzVjOGJjZjI4MiJ9LHsieC1hbXotbWV0YS1maWxlbmFtZSI6Im15X2ZpbGUucG5nIn0seyJzdWNjZXNzX2FjdGlvbl9zdGF0dXMiOiIyMDEifSx7ImJ1Y2tldCI6IjBkMGEwNjBiMDUwNiJ9LHsiWC1BbXotQWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsiWC1BbXotQ3JlZGVudGlhbCI6IkFTSUFWMjc1QVVYRkpXNU9BMjRDLzIwMjEwNDI3L3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QifSx7IlgtQW16LURhdGUiOiIyMDIxMDQyN1QyMTM0MTFaIn0seyJYLUFtei1TZWN1cml0eS1Ub2tlbiI6IklRb0piM0pwWjJsdVgyVmpFTmIvLy8vLy8vLy8vd0VhQ1hWekxXVmhjM1F0TVNKSE1FVUNJQ04yalZtak5HOEZtKzVYVVEwOUR6Q25GL3FwY3dxQUNKR2VZTG1qVHVsNUFpRUF6TmVIbjNRNWpnbkVwb0tCeFE2L0N2WkNHZGJSVk9YeWFYWG9wNFpkdVBJcTBRRUlUeEFBR2d3ME1ERTFOek14T1RNeE5qSWlESVR3NkgwVVlmL0hUUDZIOHlxdUFmbTJvNTAwdXZqaXdrc25ncisrYVRjeDN3cy9GTlFtZ01Ednk5WDRqVFdrWHRwYVMxSHo4NmhndEY4SmFyUUQwQTB5NXlNZFBqVGJDSlJsQS8wNGNKQzc3cEtkRmZSVkRGUTQ4MlBYaFNORHptcTljUEwxNFZhRmRreXl0Z1NONUtQZUloWnhTRlpqcnhwK1pua3d2N0xkUTJuY0RycThrR2h3anQxOHk5d0IxNm5rdkRqYTVPT0g1M2wvU29UV3k3a2gvdzdjVFFMbXRoN3ZvUmp4OVozNzFKWWI0eGJqckhxbk5jcjVFakRTaHFLRUJqcmdBZTh0eFhVaDF5ZktjcXFmc3pkdnVXd3J3TFZiTzBXemFvZEp1K2p1M3NnMXNDRzFsYllFSGZPV1IzV3RkY2lwcEFSeVNBd2Uza00ybXpka3l6ZzFDdkZnRnoyUjFodWIwVXcyOGY3eWtqaDFIajRkMHlVNmU5S2ZHb3hwd3FXQVBRZ1lkUEg3T1I2b21scXFGZnhhN3daOHVnVldYVjlTU0UxZGg4ZGFKUDdhei9uQjJxeExhOURBWDczcnZJNXNMbVVxd25uWkI1SHRzZTg1TkdoQ01DVkkwNmZwZTdKcHkzazhwMm04U1VKbUxjeWhGZ1R3aGJEVmlIckdkdkZhRWlVL3NzQWRDQnlodGJMQ2VMaVcwQlBBaW9VRFRzWmFpSER3b2ozSFp1ZGoifV19",
"X-Amz-Signature": "8643e95e525976bc194ef379ea07662e4f5d03cc20f40b3dcc4741dfc816bb4a"
}
}
Key
is a generated file name that is used to store the file object in our S3 bucket. We should save this name in our database and use it to retrieve the stored file when needed. We can also save the original file name either as metadata for the file object on S3 (as showcased in the example above) or explicitly in our database.
The server can retrieve privately stored files from S3 for internal processing.
const aws = require("aws-sdk");
/**
* Generates a pre-signed upload URL with limitation on uploaded file size.
*
* @param {String} filename The generate name of the file object on S3.
* @return {Object} A promise of the file Buffer.
*/
async function getFileFromS3(filename) {
const Key = filename;
const Bucket = "MY_BUCKET";
const s3 = new aws.S3();
const res = await s3.getObject({ Bucket, Key }).promise();
return res.Body;
}
A successful S3 getObject
response will look similar to this:
{
"AcceptRanges": "bytes",
"LastModified": "2021-04-28T10:33:12.000Z",
"ContentLength": 22860,
"ETag": "\"da1e6555706e9cd9db76f14c46c3bdb6\"",
"ContentType": "binary/octet-stream",
"Metadata": {
"userId": "my_user_id",
"filename": "MY_FILE.PNG"
},
"Body": {
"type": "Buffer",
"data": [...]
}
}
Use the following code to generate a pre-signed download URL with expiration time of 1 minute.
const aws = require("aws-sdk");
const { v4: uuidv4 } = require("uuid");
/**
* Generates a pre-signed download URL with time-to-lve limitation.
* This URL should be invoked via HTTP GET.
*
* @param {String} filename The generate name of the file object on S3.
* @return {Object} A pre-signed download URL.
*/
function getDownloadUrl(filename) {
const params = {
"Bucket": MY_BUCKET,
"Key": filename,
"Expires": 60
};
const s3 = new aws.S3({ "signatureVersion": "v4" });
return s3.getSignedUrl(params);
}
The response should look similar to this:
https://MY_BUCKET.s3.amazonaws.com/084deb78-8a6a-4c7b-84e9-7cc5c8bcf282?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAV275AUXFDSN73WMN%2F20210516%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210516T110045Z&X-Amz-Expires=60&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJIMEYCIQDCkn9LpuyxmLqtpwQYzire2K94duH4q69m3YUxdVTnRQIhAKSKXZmhQ6Qw7f7blIA8p%2Bp%2FemgMTO5TjavxqlXzbPj0KtEBCCwQABoMNDAxNTczMTkzMTYyIgynADJBa6UR7ElwCCsqrgGNkGGYdcQC%2F8lbMAlOsNrJvI93KtX9y4wgn6XLqUe1lnB6SGtMMK1vg0L0Gwa02nv309zizPwTwHcZLM4iDHa2bIIdyd4vv0wVpkauDgZ47uIpwKMMcfjjMnXEBjH9jgq07N2KPjg6BrCIF%2BzBTH2KUhF5k%2FOjqQZHvTcfrtO30bgafYE3s1Q8whpYzkMzTVcgV8AfK8C1ReJGxfJtMrACZ7PT41EKlip%2Fodp7O04w3PaDhQY63wEx21%2BWly4cHiYfHKJQzAzz4wN7GF6h3CSzrdyDuEIVA0vWisiBblL17lgNHEngYxHwam%2FlIS4q72%2FJNRBLhcbWN4c0Y5g4xkNfDQzBRVMm3kfrSEwZ6WE1GgVQiDEbcan8bQX0FvBgnhqTPsXaVQcoS9sYoLgbsJgmidobAftBthxPZu5jcCOzs1Iy8rzFymE0J2ZFJPEularHtP3eZlPUBnY9%2BNToeWKDe4CDwinRKq5gD3L1WOCjFdMnin7YVh86d6yatJ0t7JLH8RxtnkqqyeWBw%2Bu%2BX0PfmB83gecd&X-Amz-Signature=c6ebc35378ca5488f7bb9c64eb19df903a352b9884cdaddd38061062b826c7ea&X-Amz-SignedHeaders=host
The client should call the server to get a pre-signed upload URL. It can then use this URL to upload files to an S3 bucket via HTTP POST.
function getUploadUrl(filename) {
const url = "/api/upload-url?filename=" + filename;
return fetch(url)
.then(res => res.json())
.catch(err => {
throw new Error(
`Failed to get upload url for: ${filename} ${err}`);
});
}
/**
* Uploads file to S3 via pre-signed URL.
*
* @param {String} data The response from "getUploadUrl".
* @param {File} file The file object from the HTML input element.
* e.g. <code>document.querySelector('input[type="file"]').files[0]</code>
*/
function uploadFile(data, file) {
const form = new FormData();
Object.entries(data.fields).forEach(
entry => form.append(entry[0], entry[1]));
form.append("file", file);
const options = {
"method": "POST",
"body": form
};
return fetch(data.url, options)
.then(res => {
if (res.status !== 201) {
throw new Error(
`HTTP ${res.status} ${res.statusText}`);
}
return res;
})
.catch(err => {
throw new Error(
`Failed to upload file: ${file.name} ${err}`);
});
}
On successful upload, S3 returns HTTP 201 Created
and a response body similar to this:
<?xml version="1.0" encoding="UTF-8"?>
<PostResponse>
<Location>https://s3.amazonaws.com/MY_BUCKET/05c710a8-fb23-464b-84c7-37b1e40ff573</Location>
<Bucket>MY_BUCKET</Bucket>
<Key>05c710a8-fb23-464b-84c7-37b1e40ff573</Key>
<ETag>"da1e6555706e9cd9db76f14c46c3bdb6"</ETag>
</PostResponse>
function getDownloadUrl(filename) {
const url = "/api/download-url?filename=" + filename;
return fetch(url)
.then(res => res.text())
.catch(err => {
throw new Error(
`Failed to get download url for: ${filename} ${err}`);
});
}
function downloadFile(url) {
window.location.href = url;
}
You can find a complete list of errors on the Amazon S3 documentation page
If upload URL expires for any reason, the client can request a new upload URL from our server and then try uploading to S3 again.
If the URL policy expires, S3 returns HTTP 403 Forbidden
and a response body similar to this:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Invalid according to Policy: Policy expired.</Message>
<RequestId>J0B48CGERN0RMT9T</RequestId>
<HostId>diIQA8/OGci3Vic9h9nnIFMs793LlmW040EffsjL7cC8rbBJMMfVO7SyT7bE4Vw43WqJfo8qws4=</HostId>
</Error>
If the security token expires, S3 returns HTTP 400 Bad Request
and a response body similar to this:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>ExpiredToken</Code>
<Message>The provided token has expired.</Message>
<Token-0>IQoJb3JpZ2luX2VjENb//////////wEaCXVzLWVhc3QtMSJHMEUCICN2jVmjNG8Fm+5XUQ09DzCnF/qpcwqACJGeYLmjTul5AiEAzNeHn3Q5jgnEpoKBxQ6/CvZCGdbRVOXyaXXop4ZduPIq0QEITxAAGgw0MDE1NzMxOTMxNjIiDITw6H0UYf/HTP6H8yquAfm2o500uvjiwksngr++aTcx3ws/FNQmgMDvy9X4jTWkXtpaS1Hz86hgtF8JarQD0A0y5yMdPjTbCJRlA/04cJC77pKdFfRVDFQ482PXhSNDzmq9cPL14VaFdkyytgSN5KPeIhZxSFZjrxp+Znkwv7LdQ2ncDrq8kGhwjt18y9wB16nkvDja5OOH53l/SoTWy7kh/w7cTQLmth7voRjx9Z371JYb4xbjrHqnNcr5EjDShqKEBjrgAe8txXUh1yfKcqqfszdvuWwrwLVbO0WzaodJu+ju3sg1sCG1lbYEHfOWR3WtdcippARySAwe3kM2mzdkyzg1CvFgFz2R1hub0Uw28f7ykjh1Hj4d0yU6e9KfGoxpwqWAPQgYdPH7OR6omlqqFfxa7wZ8ugVWXV9SSE1dh8daJP7az/nB2qxLa9DAX73rvI5sLmUqwnnZB5Htse85NGhCMCVI06fpe7Jpy3k8p2m8SUJmLcyhFgTwhbDViHrGdvFaEiU/ssAdCByhtbLCeLiW0BPAioUDTsZaiHDwoj3HZudj</Token-0>
<RequestId>G9EMMHNNBTVH7WFZ</RequestId>
<HostId>OhQnjUs2Et7aAjI5BaPSHRH98Ssrzx3fwGXORhL6BPxr9PxzhYCLFTsp0Girq/aebZZf3ZvZGuc=</HostId>
</Error>
If the size of the uploaded file exceeds the maximum allowed size, S3 returns HTTP 400 Bad Request
and aborts the connection without any additional information.
We will use cURL to test our file upload flow.
$ curl -v https://MY_SERVER/api/upload-url?filename=MY_FILE.PNG
Transform the response data to cURL request arguments (FormData):
function toFormData(data) {
return Object.entries(data.fields).map(entry => `-F ${entry[0]}=${entry[1]}`).join('\n');
}
Upload the file to S3 bucket:
$ curl -v -X POST \
-F Key=05c710a8-fb23-464b-84c7-37b1e40ff573 \
-F x-amz-meta-filename=MY_FILE.PNG \
-F success_action_status=201 \
-F bucket=MY_BUCKET \
-F X-Amz-Algorithm=AWS4-HMAC-SHA256 \
-F X-Amz-Credential=ASIAV275AUXFNSN4XC7B/20210428/us-east-1/s3/aws4_request \
-F X-Amz-Date=20210428T103234Z \
-F X-Amz-Security-Token=IQoJb3JpZ2luX2VjEOP//////////wEaCXVzLWVhc3QtMSJIMEYCIQDKl+YvyZVYvdUQv8XOvJeq1He6XBOSyc2gxoK9tS4C/AIhAJr2/bkkOaBS2WTDFb5L3yGI0En3RmJJVQi+ppozA19zKtEBCFwQABoMNDAxNTczMTkzMTYyIgwDzNvwtBxeVYWkP5wqrgGTEcnavOvVlQHQad5j8NTbMswKLvgLvA7Ov+amGDN+I85WANuPhsTsOQlBo6NL1UBSQ8acY1CqC0c4iFeJi+dAj1iGgoUeyb5JMOC3tgHF3AbDdsZeVOT1+wPBz81JFnuL4+WglVFTfHi6CJe6nyb3dc1eqnyZ+/xrXjrmi3IXLDIPwBMDdfpNUUP7gI2J2NBvZyr8mnJpJKGYW3f/cmPg6Ugnk5x78fG661Ox5XQwtfKkhAY63wESkvFYZsoiqv5e4sjU+KGocvc/61/6VUq6nMnEvkdctHYmgC1hHiL66IiGqwG2XGo15SBUlJ2b4XFMHMm3fbJGitqP8hEjZvd8OLh7tLhUiL25078lGkLWkF4+IWXvhwghGvSpBbB1fuDNknwJgqN1vxS0kt3WwkXQdTh4O7LafOXI9rWglA+T2ub3RcwTdeI88Q9rmIhtHH4LEEErLMFMReeoTrDjZxzjDve3YOpPtU6yKlpsVwjkmVhbjGN3VR8k9rvu9BoCU8ioVCMyHRfhkfeU8m3pxCF/Tdgyo/2t \
-F Policy=eyJleHBpcmF0aW9uIjoiMjAyMS0wNC0yOFQxMDozMzozNFoiLCJjb25kaXRpb25zIjpbWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsMSw1MjQyODgwXSx7IktleSI6IjA1YzcxMGE4LWZiMjMtNDY0Yi04NGM3LTM3YjFlNDBmZjU3MyJ9LHsieC1hbXotbWV0YS1maWxlbmFtZSI6Im15X2ZpbGUucG5nIn0seyJzdWNjZXNzX2FjdGlvbl9zdGF0dXMiOiIyMDEifSx7ImJ1Y2tldCI6IjBkMGEwNjBiMDUwNiJ9LHsiWC1BbXotQWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsiWC1BbXotQ3JlZGVudGlhbCI6IkFTSUFWMjc1QVVYRk5TTjRYQzdCLzIwMjEwNDI4L3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QifSx7IlgtQW16LURhdGUiOiIyMDIxMDQyOFQxMDMyMzRaIn0seyJYLUFtei1TZWN1cml0eS1Ub2tlbiI6IklRb0piM0pwWjJsdVgyVmpFT1AvLy8vLy8vLy8vd0VhQ1hWekxXVmhjM1F0TVNKSU1FWUNJUURLbCtZdnlaVll2ZFVRdjhYT3ZKZXExSGU2WEJPU3ljMmd4b0s5dFM0Qy9BSWhBSnIyL2Jra09hQlMyV1RERmI1TDN5R0kwRW4zUm1KSlZRaStwcG96QTE5ekt0RUJDRndRQUJvTU5EQXhOVGN6TVRrek1UWXlJZ3dEek52d3RCeGVWWVdrUDV3cXJnR1RFY25hdk92VmxRSFFhZDVqOE5UYk1zd0tMdmdMdkE3T3YrYW1HRE4rSTg1V0FOdVBoc1RzT1FsQm82TkwxVUJTUThhY1kxQ3FDMGM0aUZlSmkrZEFqMWlHZ29VZXliNUpNT0MzdGdIRjNBYkRkc1plVk9UMSt3UEJ6ODFKRm51TDQrV2dsVkZUZkhpNkNKZTZueWIzZGMxZXFueVorL3hyWGpybWkzSVhMRElQd0JNRGRmcE5VVVA3Z0kySjJOQnZaeXI4bW5KcEpLR1lXM2YvY21QZzZVZ25rNXg3OGZHNjYxT3g1WFF3dGZLa2hBWTYzd0VTa3ZGWVpzb2lxdjVlNHNqVStLR29jdmMvNjEvNlZVcTZuTW5FdmtkY3RIWW1nQzFoSGlMNjZJaUdxd0cyWEdvMTVTQlVsSjJiNFhGTUhNbTNmYkpHaXRxUDhoRWpadmQ4T0xoN3RMaFVpTDI1MDc4bEdrTFdrRjQrSVdYdmh3Z2hHdlNwQmJCMWZ1RE5rbndKZ3FOMXZ4UzBrdDNXd2tYUWRUaDRPN0xhZk9YSTlyV2dsQStUMnViM1Jjd1RkZUk4OFE5cm1JaHRISDRMRUVFckxNRk1SZWVvVHJEalp4empEdmUzWU9wUHRVNnlLbHBzVndqa21WaGJqR04zVlI4azlydnU5Qm9DVThpb1ZDTXlIUmZoa2ZlVThtM3B4Q0YvVGRneW8vMnQifV19 \
-F X-Amz-Signature=ef408b1e4c7de3728cda3d7c9a5349567a161ba67459bd6349c49a373881c1c4 \
-F file=@MY_FILE.PNG \
https://s3.amazonaws.com/MY_BUCKET
$ aws s3 ls s3://MY_BUCKET
$ curl -v https://MY_SERVER/api/download-url?filename=084deb78-8a6a-4c7b-84e9-7cc5c8bcf282