Motivation behind this
I was searching for a solution to upload files to AWS S3 from Lightning Web Components (LWC). Through there are few solutions found through either Apex code or through JavaScript but didn't find guided approach to setup AWS S3 account as well as building reusable Lightning Web Components which can be leveraged in any projects.
Also, base lightning-file-upload component uploads files into Salesforce which cannot be used for this functionality as I want to directly store to AWS.
This idea drives me to come up with a proof-of-concept and sharing the knowledge and challenges with community members.
Let's get started.
Use Case
Business has a requirement not to store files and attachments to Salesforce, rather they have procured AWS S3 account and leverage that storage to store files. It will help Business to reduce storage limit at Salesforce org since that will also comes with cost.
Developer wants to built it with Lightning Web Components and use that anywhere as necessary. From the screen, it will ask user to choose the files or drag and drop files to upload and finally those will be uploaded to AWS S3 bucket.
Developer will also store the file information (e.g. Name, Link to AWS Location etc.) into a custom Attachment object related to the record.
Possible End Results
After building the use case, it will perform the functionality as following video:
Solution Approach
We will create fileUploadLWC component for this functionality.
Flow Diagram
Create a flow diagram like this way to understand the functionalities, flow of control and finalize design approach.

Create AWS Account and S3 Bucket
Here is the following steps I have followed.
Reach following screen and click on Create Bucket
Define Name and Region
Region is important here, initially I chose Asia Pacific (Mumbai) instead of default US East (N. Verginia) which I have faced challenges during testing (refer Solving Pain Points section). Later I changed it to default US East.
Configure Options
Let it be as it is and go next.
Set Permissions
I removed Blocking of all public access.
Review
We can change the settings if required and finally create bucket.
Create Access Key and Access Secret
Since we will be connecting through API from Salesforce so Access key and secret is needed.
After doing this, manually upload a file to verify if everything is okay.
Building Lightning Web Components
For this poc, we will attach file records under Opportunity object. To store file information, create custom Attachment object with following field information:
File Name (File_Name__c) Text
File URL (FileURL__c) URL
Opportunity (Opportunity__c) Lookup(Opportunity)
fileUploadLWC.html will prepare a screen like this:
Code of html as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| <template>
<article class="slds-tile">
<!-- file upload section -->
<lightning-card variant="Narrow" title="Lightning Web Component File Uploader for AWS"
style="width:30rem" icon-name="custom:custom14">
<div style="margin-left:5%">
<div>
<lightning-input label="" name="file to uploder" onchange={handleSelectedFiles}
type="file" multiple></lightning-input>
</div><br/>
<div class="slds-text-body_small">{fileName}
<template if:true={showSpinner}>
<lightning-spinner alternative-text="Uploading the file......" size="medium">
</lightning-spinner>
</template>
</div><br/>
<div>
<lightning-button class="slds-m-top--medium" label="Store File to AWS" onclick={handleFileUpload}
variant="brand">
</lightning-button>
</div>
</div><br/><br/>
<!--displaying uploaded files-->
<template if:true={tableData}>
<lightning-card title="Following files uploaded:">
<div style="width: auto;">
<ul class="slds-m-around_small">
<template for:each={tableData} for:item="attachment">
<li key={attachment.Id}>
{attachment.File_Name__c},
<lightning-formatted-url value={attachment.FileURL__c} target="_blank">{attachment.FileURL__c}</lightning-formatted-url>
</li>
</template>
</ul>
</div>
</lightning-card>
</template>
</lightning-card>
</article>
</template>
|
fileUploadLWC.js
It performs the operation as given in the flow diagram. Also added context sensitive comments in-line.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
| /* eslint-disable no-console */
import { LightningElement, api, track } from 'lwc';
import uploadFileToAWS from '@salesforce/apex/AWSFileUploadController.uploadFileToAWS';
import displayUploadedFiles from '@salesforce/apex/AWSFileUploadController.displayUploadedFiles';
import {ShowToastEvent} from 'lightning/platformShowToastEvent';
export default class fileUploadLWC extends LightningElement {
@api recordId; //get the recordId for which files will be attached.
selectedFilesToUpload = []; //store selected files
@track showSpinner = false; //used for when to show spinner
@track fileName; //to display the selected file name
@track tableData; //to display the uploaded file and link to AWS
file; //holding file instance
myFile;
fileType;//holding file type
fileReaderObj;
base64FileData;
// get the file name from the user's selection
handleSelectedFiles(event) {
if(event.target.files.length > 0) {
this.selectedFilesToUpload = event.target.files;
this.fileName = this.selectedFilesToUpload[0].name;
this.fileType = this.selectedFilesToUpload[0].type;
console.log('fileName=' + this.fileName);
console.log('fileType=' + this.fileType);
}
}
//parsing the file and prepare for upload.
handleFileUpload(){
if(this.selectedFilesToUpload.length > 0) {
this.showSpinner = true;
this.file = this.selectedFilesToUpload[0];
//create an intance of File
this.fileReaderObj = new FileReader();
//this callback function in for fileReaderObj.readAsDataURL
this.fileReaderObj.onloadend = (() => {
//get the uploaded file in base64 format
let fileContents = this.fileReaderObj.result;
fileContents = fileContents.substr(fileContents.indexOf(',')+1)
//read the file chunkwise
let sliceSize = 1024;
let byteCharacters = atob(fileContents);
let bytesLength = byteCharacters.length;
let slicesCount = Math.ceil(bytesLength / sliceSize);
let byteArrays = new Array(slicesCount);
for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
let begin = sliceIndex * sliceSize;
let end = Math.min(begin + sliceSize, bytesLength);
let bytes = new Array(end - begin);
for (let offset = begin, i = 0 ; offset < end; ++i, ++offset) {
bytes[i] = byteCharacters[offset].charCodeAt(0);
}
byteArrays[sliceIndex] = new Uint8Array(bytes);
}
//from arraybuffer create a File instance
this.myFile = new File(byteArrays, this.fileName, { type: this.fileType });
//callback for final base64 String format
let reader = new FileReader();
reader.onloadend = (() => {
let base64data = reader.result;
this.base64FileData = base64data.substr(base64data.indexOf(',')+1);
this.fileUpload();
});
reader.readAsDataURL(this.myFile);
});
this.fileReaderObj.readAsDataURL(this.file);
}
else {
this.fileName = 'Please select a file to upload!';
}
}
//this method calls Apex's controller to upload file in AWS
fileUpload(){
//implicit call to apex
uploadFileToAWS({ parentId: this.recordId,
strfileName: this.file.name,
fileType: this.file.type,
fileContent: encodeURIComponent(this.base64FileData)})
.then(result => {
console.log('Upload result = ' +result);
this.fileName = this.fileName + ' - Uploaded Successfully';
//call to show uploaded files
this.getUploadedFiles();
this.showSpinner = false;
// Showing Success message after uploading
this.dispatchEvent(
new ShowToastEvent({
title: 'Success!!',
message: this.file.name + ' - Uploaded Successfully!!!',
variant: 'success',
}),
);
})
.catch(error => {
// Error to show during upload
window.console.log(error);
this.dispatchEvent(
new ShowToastEvent({
title: 'Error in uploading File',
message: error.message,
variant: 'error',
}),
);
this.showSpinner = false;
});
}
//retrieve uploaded file information to display to the user
getUploadedFiles(){
displayUploadedFiles({parentId: this.recordId})
.then(data => {
this.tableData = data;
console.log('tableData=' + this.tableData);
})
.catch(error => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Error in displaying data!!',
message: error.message,
variant: 'error',
}),
);
});
}
}
|
AWSFileUploadController.cls
uploadFileToAWS method takes file information from js controller and creates end point and post the file as blob with Http callout.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
| public with sharing class AWSFileUploadController {
//This method is used to post file to AWS
@AuraEnabled
public static boolean uploadFileToAWS(Id parentId,
String strfileName, String fileType,
String fileContent){
System.debug('parentId=' + parentId);
System.debug('strfileName=' + strfileName);
System.debug('fileType=' + fileType);
HttpRequest req = new HttpRequest();
Blob base64Content = EncodingUtil.base64Decode(EncodingUtil.urlDecode(fileContent, 'UTF-8'));
String attachmentBody = fileContent;
String formattedDateString = Datetime.now().formatGMT('EEE, dd MMM yyyy HH:mm:ss z');
String dateString = Datetime.now().format('ddMMYYYYHHmmss');
String filename = dateString + '_' + parentId + '_' + strfileName;
//AWS specific information
String key = 'Provide AWS key'; //AWS key
String secret = 'Provide AWS Secret key'; //AWS Secret key
String bucketname = 'Provide AWS bucket'; //AWS bucket name
String host = 's3.amazonaws.com:443'; //'s3.amazonaws.com:443'
String method = 'PUT';
String endPoint = 'https://' + bucketname + '.' + host + '/'+ filename;
req.setMethod(method);
req.setEndpoint(endPoint);
system.debug('Endpoint='+endPoint);
//create header information
req.setHeader('Host', bucketname + '.' + host);
req.setHeader('Access-Control-Allow-Origin', '*');
req.setHeader('Content-Length', String.valueOf(attachmentBody.length()));
req.setHeader('Content-Encoding', 'UTF-8');
req.setHeader('Content-type', fileType);
req.setHeader('Connection', 'keep-alive');
req.setHeader('Date', formattedDateString);
req.setHeader('ACL', 'public-read');
//store file as blob
req.setBodyAsBlob(base64Content);
//prepare for signing information
String stringToSign = 'PUT\n\n' +
fileType + '\n' +
formattedDateString + '\n' + '/' + bucketname + '/' + filename;
String encodedStringToSign = EncodingUtil.urlEncode(stringToSign, 'UTF-8');
Blob mac = Crypto.generateMac('HMACSHA1', blob.valueof(stringToSign),blob.valueof(secret));
String signedKey = EncodingUtil.base64Encode(mac);
//assign Authorization information
String authHeader = 'AWS' + ' ' + key + ':' + signedKey;
req.setHeader('Authorization',authHeader);
//finally send information to AWS
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug('*Resp:' + String.ValueOF(res.getBody()));
System.debug('RESPONSE STRING: ' + res.toString());
System.debug('RESPONSE STATUS: ' + res.getStatus());
System.debug('STATUS_CODE: ' + res.getStatusCode());
if(res.getStatusCode() == 200){
insertAttachmentRecord (parentId,strfileName,endPoint);
return true;
}
return false;
}
//This method inserts file information to Custom Attachment object
public static void insertAttachmentRecord (Id parentId, String fileName, String fileURL){
Attachment__c attachment = new Attachment__c();
attachment.Opportunity__c = parentId;
attachment.FileURL__c = fileURL;
attachment.File_Name__c = fileName;
insert attachment;
}
//This method retrieves Attachment record based on OpportunityId
@AuraEnabled
public static List<Attachment__c> displayUploadedFiles(Id parentId){
return [SELECT Id, File_Name__c, FileURL__c FROM Attachment__c
WHERE Opportunity__c =:parentId];
}
}
|
After code is ready, change meta.xml file to expose the component to record detail page.
Since we are making callouts so, endpoint URL must be defined in the Remote Site Settings.
Solving Pain Points
Following issues I have faced and possible solutions:
1. For my AWS account, the region of the bucket was other the default. It may not be activated within 24 hours. So, if we try to make callouts, you might face following Temporary Redirect issues with response status code 307.
To solve this, I have changed the region to US East.
2. If the permission of bucket is restricted then, after posting the file from application if you try to access the link from the web browser then you might face access denied error.
To resolve this, go the bucket policy and check the JSON if mentioned.
Action": [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:GetObjectAcl"
]
Without GetObjectAcl, it will not allow to access URL hitting from browser.
3. File should be read chunk wise otherwise there will be error in uploading the file.
4. Preparation of signing information to generate signedKey is crucial.
That's all, I have faced.
Finally, in the Opportunity record detail page, add a new tab and expose the fileUploadLWC component and run the application.
Hope, it helps!
Further Reading