Downloading SObject Files in Salesforce Made Easy with JSZip
Introduction
In Salesforce, you often need to download files associated with a specific record ๐. This can be challenging, especially with complex file structures and metadata ๐งฉ. In this documentation, we will explore a code snippet that allows you to download files linked to an object in Salesforce. We will explain the code in detail and provide step-by-step instructions on how to implement this functionality in your Salesforce org ๐.
Start blogging with Hashnode
Apex Class to Handle Retrieving Files for Download
Let's start by examining the code snippet below:
/**********************************************************************************************
* @description Controller class for the DownloadFiles Lightning Web Comp
* @autor: Christian Pelayo
* @date: 2024-07-15
***********************************************************************************************/
public with sharing class DownloadFilesController {
@AuraEnabled
/**********************************************************************************************
* @description Retrieves the file IDs and their associated metadata for a given record ID.
* This includes querying the record's name, associated ContentDocumentLink records,
* and their latest ContentVersion data. The method returns a RecordFileWrapper
* containing the record's name and a list of FileWrapper objects with file details.
* @param objectId The ID of the record for which to retrieve file information.
* @return RecordFileWrapper An object containing the record's name and a list of FileWrapper
* objects, each representing a file associated with the record.
***********************************************************************************************/
public static RecordFileWrapper getFileIdsByRecordId(Id objectId) {
List<FileWrapper> files = new List<FileWrapper>();
// Get the object's name dynamically and query the record to get its name field
SObject record = Database.query('SELECT Name FROM ' + objectId.getSObjectType().getDescribe().getName() + ' WHERE Id = :objectId LIMIT 1');
String recordName = (String) record.get('Name');
// Query ContentDocumentLink records associated with the record
List<ContentDocumentLink> contentDocumentLinks = [
SELECT ContentDocumentId, ContentDocument.Title, ContentDocument.LatestPublishedVersionId
FROM ContentDocumentLink
WHERE LinkedEntityId = :objectId
WITH SYSTEM_MODE
ORDER BY SystemModstamp DESC
];
// Store LatestPublishedVersionIds
Set<Id> versionIds = new Set<Id>();
for (ContentDocumentLink link : contentDocumentLinks) {
versionIds.add(link.ContentDocument.LatestPublishedVersionId);
}
// Query ContentVersion records
Map<Id, ContentVersion> contentVersions = new Map<Id, ContentVersion>(
[SELECT Id, VersionData, FileExtension FROM ContentVersion WHERE Id IN :versionIds]
);
for (ContentDocumentLink link : contentDocumentLinks) {
ContentVersion contentVersion = contentVersions.get(link.ContentDocument.LatestPublishedVersionId);
if (contentVersion != null) {
String base64Data = EncodingUtil.base64Encode(contentVersion.VersionData);
String fileName = link.ContentDocument.Title + '.' + contentVersion.FileExtension;
files.add(new FileWrapper(fileName, base64Data));
}
}
return new RecordFileWrapper(recordName, files);
}
/**********************************************************************************************
* @description Wrapper class for handling file-related operations in the DownloadFilesController.
***********************************************************************************************/
public class FileWrapper {
@AuraEnabled
public String fileName { get; set; }
@AuraEnabled
public String fileContent { get; set; }
/**********************************************************************************************
* @description Constructs a FileWrapper object with the specified file name and file content.
* @param fileName The name of the file.
* @param fileContent The content of the file.
**********************************************************************************************/
public FileWrapper(String fileName, String fileContent) {
this.fileName = fileName;
this.fileContent = fileContent;
}
}
/**********************************************************************************************
* @description Wrapper class for handling record file operations in the DownloadFilesController.
***********************************************************************************************/
public class RecordFileWrapper {
@AuraEnabled
public String recordName { get; set; }
@AuraEnabled
public List<FileWrapper> files { get; set; }
/**********************************************************************************************
* @description Constructs a RecordFileWrapper object with the specified record name and list of file wrappers.
* @param recordName The name of the record associated with the files.
* @param files A list of FileWrapper objects representing the files associated with the record.
**********************************************************************************************/
public RecordFileWrapper(String recordName, List<FileWrapper> files) {
this.recordName = recordName;
this.files = files;
}
}
}
The code snippet above defines a controller class called DownloadFilesController
. This class has a method named getFileIdsByRecordId
, which retrieves file IDs and metadata for a given record ID. The method is marked with @AuraEnabled
, allowing it to be called from a Lightning component.
getFileIdsByRecordId Method
The getFileIdsByRecordId
method retrieves file IDs and metadata for a given record ID. Here's how it works:
The method takes an
Id
parameter calledobjectId
, representing the ID of the record for which you want to get file information.It dynamically retrieves the object's name by querying the record's name field using the
objectId
.It then queries the
ContentDocumentLink
records associated with the record. These records link the record to the files.The method stores the latest published version IDs of the files by iterating through the
ContentDocumentLink
records.Next, it queries the
ContentVersion
records using the version IDs obtained in the previous step. These records contain the actual file data.For each
ContentVersion
record, the method creates aFileWrapper
object, which includes the file name and the base64-encoded file content.Finally, the method returns a
RecordFileWrapper
object, which contains the record's name and a list ofFileWrapper
objects representing the files associated with the record.
Wrapper Classes
The code snippet includes two wrapper classes: FileWrapper
and RecordFileWrapper
. These classes help structure and organize the data returned by the getFileIdsByRecordId
method.
The FileWrapper
class represents a single file and contains the file name and the base64-encoded file content. The RecordFileWrapper
class represents the record and includes the record's name and a list of FileWrapper
objects.
LWC uses JSZip to download the files
To implement the functionality of downloading files linked to an object in Salesforce, follow these steps:
Create a new Lightning Web Component (LWC) called
DownloadFilesQuickAction
.In the JavaScript file of the
DownloadFilesQuickAction
LWC, import thegetFileIdsByRecordId
method from theDownloadFilesController
Apex class.Implement the necessary logic in the JavaScript file to call the
getFileIdsByRecordId
method and handle the returned data.Use the retrieved file data to provide a download feature in the user interface of the
DownloadFilesQuickAction
LWC. You can create a button or a link that triggers the file download process.Customize the user interface of the
DownloadFilesQuickAction
LWC to make it visually appealing and user-friendly. You can add additional features such as file previews or search functionality.
For reference on how to use JSZip, you can check the following link: JSZip.
downloadFilesQuickAction.js
import { LightningElement, api } from "lwc";
import getFileIdsByRecordId from "@salesforce/apex/DownloadFilesController.getFileIdsByRecordId";
import { NavigationMixin } from "lightning/navigation";
import jszip from "@salesforce/resourceUrl/OMSResources";
import { loadScript } from "lightning/platformResourceLoader";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
/**
* A Lightning Web Component (LWC) that provides a quick action for downloading files.
* This component extends the NavigationMixin to enable navigation capabilities.
* @autor Christian Pelayo
*
* @class
* @extends {LightningElement}
*/
export default class DownloadFilesQuickAction extends NavigationMixin(LightningElement) {
@api recordId;
zipInitialized = false;
jsZip;
/**
* Invokes the file download process, ensuring the JSZip library is loaded before proceeding.
* If the JSZip library is not initialized, it loads the library first, then downloads the files.
* If the library is already initialized, it directly proceeds to download the files.
* Handles any errors that occur during the loading of the JSZip library.
*
* @returns {void}
*/
@api
invoke() {
if (!this.zipInitialized) {
this.loadJsZipLibrary()
.then(() => this.downloadFiles())
.catch((error) => this.handleError("Error loading JSZip library.", error));
} else {
this.downloadFiles();
}
}
/**
* Loads the JSZip library and initializes the JSZip instance.
*
* This function dynamically loads the JSZip library from the specified path and initializes
* a new instance of JSZip, setting the `jsZip` property on the current object and marking
* the `zipInitialized` flag as true upon successful loading.
*
* @returns {Promise<void>} A promise that resolves when the JSZip library is successfully loaded and initialized.
*/
loadJsZipLibrary() {
return loadScript(this, `${jszip}/OMSResources/js/jszip.min.js`).then(() => {
this.jsZip = new window.JSZip();
this.zipInitialized = true;
});
}
/**
* Downloads files associated with a specific record ID, creates a ZIP file if files are found,
* and handles various outcomes such as showing a toast message or closing the quick action.
*
* @returns {void}
*/
downloadFiles() {
getFileIdsByRecordId({ objectId: this.recordId })
.then((result) => {
if (result.files.length > 0) {
this.createZipFile(result.recordName, result.files);
} else {
this.showToast("Info", "No files to download.", "info");
this.closeQuickAction();
}
})
.catch((error) => this.handleError("Error downloading files.", error));
}
/**
* Creates a ZIP file from the provided files and triggers its download.
*
* @param {string} objectName - The name of the folder to be created inside the ZIP file.
* @param {Array} files - An array of file objects, each containing `fileName` and `fileContent` properties.
* @param {string} files[].fileName - The name of the file to be added to the ZIP.
* @param {string} files[].fileContent - The base64 encoded content of the file.
*
* @returns {void}
*/
createZipFile(objectName, files) {
const folder = this.jsZip.folder(objectName);
files.forEach((file) => {
folder.file(file.fileName, file.fileContent, { base64: true });
});
this.jsZip
.generateAsync({ type: "blob" })
.then((content) => this.downloadZipFile(objectName, content))
.catch((error) => this.handleError("Error creating ZIP file.", error));
}
/**
* Initiates the download of a ZIP file and provides user feedback.
*
* @param {string} objectName - The name of the object to be used as the filename for the downloaded ZIP file.
* @param {Blob} content - The content of the ZIP file as a Blob object.
*/
downloadZipFile(objectName, content) {
const element = document.createElement("a");
element.href = URL.createObjectURL(content);
element.download = `${objectName}.zip`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
this.showToast("Success", "Download was successful.", "success");
this.closeQuickAction();
}
/**
* Displays a toast notification with the specified title, message, and variant.
*
* @param {string} title - The title of the toast notification.
* @param {string} message - The message content of the toast notification.
* @param {string} variant - The variant of the toast notification (e.g., 'success', 'error', 'warning', 'info').
*/
showToast(title, message, variant) {
const event = new ShowToastEvent({
title: title,
message: message,
variant: variant
});
this.dispatchEvent(event);
}
/**
* Closes the quick action modal by dispatching a custom 'close' event.
* This method creates a new CustomEvent with the type 'close' and dispatches it
* to notify any listeners that the quick action should be closed.
*/
closeQuickAction() {
const closeEvent = new CustomEvent("close");
this.dispatchEvent(closeEvent);
}
/**
* Handles errors by logging them, displaying a toast notification, and closing the quick action.
*
* @param {string} message - The error message to be displayed and logged.
* @param {Error} error - The error object to be logged.
* @returns {void}
*/
handleError(message, error) {
console.error(message, error);
this.showToast("Error", message, "error");
this.closeQuickAction();
}
}
downloadFilesQuickAction.js-meta.xml
<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>61.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Download Files Quick Action</masterLabel>
<description>This component is used to download files from the record page.</description>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
<target>lightning__RecordAction</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordAction">
<actionType>Action</actionType>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Conclusion
Downloading files linked to an object in Salesforce is now easier with the code snippet provided in this documentation. By using the getFileIdsByRecordId
method and customizing the user interface, you can improve the file management capabilities of your Salesforce org. We hope this documentation has helped you understand the code and guided you through the implementation process.
If you have any further questions or need help with Salesforce development, feel free to reach out. Happy coding!
References
For further reading and a detailed step-by-step guide on creating and downloading multiple files as a ZIP folder in Salesforce, you can refer to the following resource:
Subscribe to my newsletter
Read articles from Christian Pelayo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Christian Pelayo
Christian Pelayo
5x Salesforce Certified | Software Engineer | Tech Enthusiast Github: https://github.com/pelayochristian