List/Download Azure File Storage files from Salesforce

This article will help you list and download files from Azure file storage using File Service REST API. You don’t have to physically store the file in salesforce, files will be stored on Azure and you just have to click a link in salesforce to download that file.

It will help you to overcome file storage limits in salesforce and extend it using Azure files. In the following example, I am listing files based on Account number and allowing users to click on file name to download contents from salesforce.

Before we jump into actual lightning component code, let’s have a quick look at steps you need to do in azure file storage account. In case you haven’t done it before. You can skip the first 2 steps if you already have your storage account configured.

Azure file storage setup

  1. Go to your azure portal , click on storage account and Add new account by filling required information
    Azure Storage Account Create
  2. After creating a new storage account, go to the storage account and create a new File Share. This will be your root folder for files. You can upload files here.
    Azure storage file share
  3. We will be using a shared access key technique to connect with an Azure storage account from salesforce. So now we need to grab that access key first.
  4. Go back to your storage account and click on Access Keys
    Azure access keys

Now let’s go through the code of lightning components. More details on using File Service API can be found here. https://docs.microsoft.com/en-us/rest/api/storageservices/file-service-rest-api

And details about authorization using the shared key can be found here. https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key

Lightning Component

<aura:component controller="AzureFileComponentController" implements="force:appHostable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId" access="global" >
	<aura:attribute name="fileNames" type="Object[]" />
    <aura:handler name="init" value="{!this}" action="{!c.getRelatedFileList}"/>
    
    <aura:if isTrue="{!empty(v.fileNames)}">
        No Files to display.
    </aura:if>
    
    <aura:if isTrue="{!not(empty(v.fileNames))}">
        <lightning:accordion aura:id="accordion">
            <aura:iteration items="{!v.fileNames}" var="fileName">
                <lightning:accordionSection name="{!fileName}" label="{!fileName}">
                    <aura:set attribute="actions">
                    	<lightning:button variant="brand" label="Download" title="{!fileName}" onclick="{! c.handleDownloadClick }" />
                    </aura:set>
                </lightning:accordionSection>
            </aura:iteration>
        </lightning:accordion>
    </aura:if>
    
</aura:component>

Lightning JS Controller

({
	getRelatedFileList : function(component, event, helper) {
		helper.getFileListFromAzure(component, event);
	},
    
    handleDownloadClick : function(component, event, helper){
    	helper.downloadFileContent(component, event);
	}
})

JS Helper

({
	getFileListFromAzure : function(component, event) {
		var accountId = component.get('v.recordId');
        
        var action = component.get('c.getFileNamesFromAzure');
        
        action.setParams({
            relatedAccountId: accountId
        });
        
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === 'SUCCESS') {
                var responseResult = response.getReturnValue();
                
                if(responseResult == 'noRecords'){
                    component.set('v.fileNames',[]);
                }
                else{
                    component.set('v.fileNames',responseResult);
                }
            }
        });
        
        $A.enqueueAction(action);
	},
    
    downloadFileContent : function(component, event) {
        
		var fileName = event.getSource().get("v.name");
        
        var action = component.get('c.getFileContentFromAzure');
        
        action.setParams({
            requestedFileName: fileName
        });
        
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === 'SUCCESS') {
                var responseResult = response.getReturnValue();
                
                if(responseResult != 'noRecords'){
                    console.log(responseResult);
					
					var url = 'data:application/octet-stream;base64,' + responseResult;
					var urlEvent = $A.get('e.force:navigateToURL');
					
					urlEvent.setParams({
						"url": url
					});
					
					urlEvent.fire();
                }
                else{
                    alert('Something went wrong.');
                }
            }
        });
        
        $A.enqueueAction(action);
	}
})

Apex Controller

public class AzureFileComponentController{
    
    @AuraEnabled
    public static Object getFileNamesFromAzure(String relatedAccountId){
        
        String fileNamePrefix = getFileNamePrefix(relatedAccountId);
        
        if(String.isNotBlank(fileNamePrefix)){
            
            String strGMTDate = DateTime.now().formatGMT('EEE, dd MMM yyyy HH:mm:ss z');
			
			//in azure file listing endpoint, we can pass a prefix variable
			//so it will only fetch files whose names are starting with given prefix
			//in this example, I am passing Account number as prefix variable, so I will get only files for respective account
            String strEndpoint = 'https://.file.core.windows.net/?restype=directory&comp=list&prefix=' + fileNamePrefix;
            
            String stringToSign = 'GET\n\n\n\n\n\n\n\n\n\n\n\nx-ms-date:' + strGMTDate + '\nx-ms-version:2019-02-02\n//';
            stringToSign += '\ncomp:list\nprefix:' + fileNamePrefix + '\nrestype:directory';
            
            String accountSharedKey = 'fuuiyiuyuyuiklklgCw91po+m2IppVHIB1wA=='; // replace with your accounts shared key
            Blob decodedAccountSharedKey = EncodingUtil.base64Decode(accountSharedKey);
            
            String authToken = EncodingUtil.base64Encode(crypto.generateMac('HmacSHA256',Blob.valueOf(stringToSign), decodedAccountSharedKey));
            
            String authHeader = 'SharedKey :' + authToken;
            
            HttpRequest req = new HttpRequest();
            req.setEndpoint(strEndpoint);
            req.setMethod('GET');
            req.setHeader('Authorization', authHeader);
            req.setHeader('x-ms-date', strGMTDate);
            req.setHeader('x-ms-version','2019-02-02');
            System.debug(req);
            Http http = new Http();
            HTTPResponse res = http.send(req);
            System.debug(res);
            System.debug(res.getBody());
            
            String responseBody = res.getBody();
            
			//extract the file names from returned xml response and prepare a list of all file names
            Matcher regex = Pattern.compile('(?si)(.*?)<\\/Name>').matcher(responseBody);
            List lstFileNames = new List();
            
            while (regex.find()) {
                lstFileNames.add(regex.group(1));
            }
            
            system.debug(lstFileNames);
            return lstFileNames;
        }
        
        return 'noRecords';
    }
    
    public static String getFileNamePrefix(String relatedAccountId){
        
        String filePrefix = '';
        
        try{
            Account accountRecord = [SELECT Id,AccountNumber FROM Account WHERE Id =: relatedAccountId];
            
            if(accountRecord != null){
                
                filePrefix = accountRecord.AccountNumber;
                filePrefix = filePrefix.replaceAll(' ', '_');
                
                return filePrefix;
            }
        }
        catch(Exception e){
            system.debug(e.getMessage());
        }
        
        return filePrefix;
    }
	
	@AuraEnabled
    public static Object getFileContentFromAzure(String requestedFileName){
		
		if(String.isNotBlank(requestedFileName)){
        
            String strGMTDate = DateTime.now().formatGMT('EEE, dd MMM yyyy HH:mm:ss z');
    
            String strEndpoint = 'https://.file.core.windows.net//' + requestedFileName;
            
            String stringToSign = 'GET\n\n\n\n\n\n\n\n\n\n\n\nx-ms-date:' + strGMTDate + '\nx-ms-version:2019-02-02\n///' + requestedFileName;
            
            String accountSharedKey = 'fuuiyiuyuyuiklklgCw91po+m2IppVHIB1wA=='; // replace with your accounts shared key
            Blob decodedAccountSharedKey = EncodingUtil.base64Decode(accountSharedKey);
            
            String authToken = EncodingUtil.base64Encode(crypto.generateMac('HmacSHA256',Blob.valueOf(stringToSign), decodedAccountSharedKey));
            
            String authHeader = 'SharedKey :' + authToken;
            
            HttpRequest req = new HttpRequest();
            req.setEndpoint(strEndpoint);
            req.setMethod('GET');
            req.setHeader('Authorization', authHeader);
            req.setHeader('x-ms-date', strGMTDate);
            req.setHeader('x-ms-version','2019-02-02');
            System.debug(req);
            Http http = new Http();
            HTTPResponse res = http.send(req);
            System.debug(res);
            
            return EncodingUtil.base64Encode(res.getBodyAsBlob());
        }
        
        return 'noRecords';
	}
}

Post a comment if you are stuck or need any help.