Hello devs,
This post will be about Salesforce Files | Note & Attachments! I would like to explain the relationship between ContentDocument, ContentDocumentLink, ContentDistribution, ContentVersion, and our Object for which a file is attached.
I also want to show you how we can display files in the Salesforce Community and allows community users to download it.
Ready? Let’s get start!
Introduction
Before we go to code, you need to understand how it works.

ContentDocument

“Represents a document that has been uploaded to a library in Salesforce CRM Content or Salesforce Files” ~ Salesforce
To put it more simply: It is just our file.
ContentDocumentLink

“Represents the link between a Salesforce CRM Content document or Salesforce file and where it’s shared. A file can be shared with other users, groups, records, and Salesforce CRM Content libraries” ~ Salesforce
This is a junction object between our file and our record.
Each record (instance of standard or custom object) has Files/Notes & Attachments related list. When you adding some file, ContentDocumentLink between record and file is automatically created.
Do it programmatically (create a connection between record with which you want share file and file which you want to share.
ContentDocumentLink contentDocumentLink = new ContentDocumentLink(
LinkedEntityId = recordIdWithWhichYouWantToShareFile,
ContentDocumentId = contentDocumentIdWhichYouWantShare,
shareType = 'V',
Visibility = 'AllUsers'
);
insert contentDocumentLink ;
- ShareType:
V – Viewer permission. The user can explicitly view but not edit the shared file,
C – Collaborator permission. The user can explicitly view and edit the shared file,
I – Inferred permission. The user’s permission is determined by the related record - Visibility:
AllUsers – The file is available to all users who have permission to see the file, InternalUsers – The file is available only to internal users who have permission to see the file,
SharedUsers – The file is available to all users who can see the feed to which the file is posted.
More details about ContentDocumentFields you can find here.
Note: If the file was already shared you will receive an error.
If you want to share your file with many records (Accounts, Contacts, etc)
myFile
– new ContentDocumentLink (ContentDocumentId = myFile.id, LinkedEntityId = Acccount.Id)
– new ContentDocumentLink (ContentDocumentId = myFile.id && LinkedEntityId = Case.Id)
– new ContentDocumentLink (ContentDocumentId = myFile.id && LinkedEntityId = Contact.Id)
If you want to share your record with many files
myRecord
– new ContentDocumentLink (ContentDocumentId = myFile1.id, LinkedEntityId = myRecord .Id)
– new ContentDocumentLink (ContentDocumentId = myFile2.id && LinkedEntityId = myRecord .Id)
– new ContentDocumentLink (ContentDocumentId = myFile3.id && LinkedEntityId = myRecord .Id)
ContentVersion
“Represents a specific version of a document in Salesforce CRM Content or Salesforce Files.” ~ Salesforce
ContentDistribution
“Represents information about sharing a document externally. ” ~ Salesforce
Here we can find ContentDownloadUrl or DistributionPublicUrl.
Standard/Custom Object
You can add a file to our record by Files or Notes & Attachments related list.
DEMO!
Download files in the Community!
We have record details like Id, Name and Description and list of related files.
File id is a link, when the user will click it, the file will be downloaded.
Main Assumption
1. I created Custom Object: Document__c and Record Type for it called: Community
2. Security! I prepared sharing rules based on criteria
Record Type = Community => share with all Community Users

3. Security! User can see only those files which are related to a Document__c records, but ContentDocumentLink.Visibility needs to be set to ‘AllUsers’.

To automatically (in other cases you need to do it manually) enabled Customer Access I added the following trigger
trigger ContentDocumentLinkTrigger on ContentDocumentLink (before insert) {
if (Trigger.isBefore && Trigger.isInsert) {
String lhwDocumentPrefix = Schema.getGlobalDescribe().get('Document__c').getDescribe().getKeyPrefix();
for (ContentDocumentLink cdl : Trigger.New) {
if (String.valueOf(cdl.LinkedEntityId).startsWithIgnoreCase(lhwDocumentPrefix)) {
cdl.Visibility = 'AllUsers';
}
}
}
}
4. To receive ContentDownloadUrl we need to use following SOQL statement.
SELECT Id, Name, ContentDownloadUrl, ContentDocumentId
FROM ContentDistribution
Apex Code
//Response Wrapper
public class DocumentWrapper {
@AuraEnabled
public Id id;
@AuraEnabled
public String name;
@AuraEnabled
public String description;
@AuraEnabled
public List<FileWrapper> files;
public class FileWrapper {
@AuraEnabled
public Id id;
@AuraEnabled
public String name;
@AuraEnabled
public String fileExtension;
}
}
//Class allows us to get all Documents records and related files, contains also logic responsible for get downloadUrl
public with sharing class FilesController {
@AuraEnabled(cacheable=true)
public static List<DocumentWrapper> getAllDocumentsAndRelatedFiles(){
Map<Id, Document__c> documents = new Map<Id, Document__c>([ SELECT Id, Name, Description__c
FROM Document__c ]);
Map<Id, ContentDocumentLink> contentDocumentLinks = new Map<Id, ContentDocumentLink>([ SELECT Id, LinkedEntityId, ContentDocumentId
FROM ContentDocumentLink
WHERE LinkedEntityId IN: documents.keySet() ]);
Map<Id, ContentDocument> contentDocuments = new Map<Id, ContentDocument>([ SELECT Id, Title, FileExtension
FROM ContentDocument
WHERE Id IN: getContentDocumentIds(contentDocumentLinks.values()) ]);
Map<Id, List<ContentDocument>> documentIdToContentDocumentsList = prepareDocumentIdToContentDocumentListMap(contentDocumentLinks.values(), contentDocuments);
return prepareDocumentWrapperResponse(documentIdToContentDocumentsList, documents);
}
@AuraEnabled
public static ContentDistribution getContentDistributionForFile(Id contentDocumentId){
ContentVersion contentVersion = [ SELECT Id, ContentDocumentId, IsMajorVersion, IsLatest
FROM ContentVersion
WHERE ContentDocumentId =: contentDocumentId
AND IsLatest = true
LIMIT 1 ];
List<ContentDistribution> contentDistribution = [ SELECT Id, Name, ContentDownloadUrl, ContentVersionId
FROM ContentDistribution
WHERE ContentVersionId =: contentVersion.Id ];
if (!contentDistribution.isEmpty()) {
return contentDistribution[0];
}
// else create new contentDistribution
ContentDistribution newContentDistribution = new ContentDistribution( Name = 'Test',
ContentVersionId = contentVersion.Id,
PreferencesAllowViewInBrowser = true );
insert newContentDistribution;
return [ SELECT Id, Name, ContentDownloadUrl, ContentDocumentId
FROM ContentDistribution
WHERE Id =: newContentDistribution.Id
LIMIT 1 ];
}
private static List<Id> getContentDocumentIds(List<ContentDocumentLink> contentDocumentsLinks) {
List<Id> contentDocumentsIds = new List<Id>();
for (ContentDocumentLink contentDocumentLink : contentDocumentsLinks) {
contentDocumentsIds.add(contentDocumentLink.ContentDocumentId);
}
return contentDocumentsIds;
}
private static Map<Id, List<ContentDocument>> prepareDocumentIdToContentDocumentListMap(List<ContentDocumentLink> contentDocumentsLinks, Map<Id, ContentDocument> contentDocuments) {
Map<Id, List<ContentDocument>> documentIdToContentDocumentsList = new Map<Id, List<ContentDocument>>();
for (ContentDocumentLink contentDocumentLink : contentDocumentsLinks) {
List<ContentDocument> currentContentDocumentList = documentIdToContentDocumentsList.get(contentDocumentLink.LinkedEntityId);
if (currentContentDocumentList == null) {
currentContentDocumentList = new List<ContentDocument>();
}
currentContentDocumentList.add(
contentDocuments.get(contentDocumentLink.ContentDocumentId)
);
documentIdToContentDocumentsList.put(contentDocumentLink.LinkedEntityId, currentContentDocumentList);
}
return documentIdToContentDocumentsList;
}
private static List<DocumentWrapper> prepareDocumentWrapperResponse(Map<Id, List<ContentDocument>> documentIdToContentDocumentsList, Map<Id, Document__c> documents) {
List<DocumentWrapper> documentsAndFiles = new List<DocumentWrapper>();
for (Id documentId : documentIdToContentDocumentsList.keySet()) {
DocumentWrapper documentWrapper = new DocumentWrapper();
documentWrapper.id = documentId;
documentWrapper.name = documents.get(documentId).Name;
documentWrapper.description = documents.get(documentId).Description__c;
documentWrapper.files = new List<DocumentWrapper.FileWrapper>();
for (ContentDocument contentDocument : documentIdToContentDocumentsList.get(documentId)) {
DocumentWrapper.FileWrapper fileWrapper = new DocumentWrapper.FileWrapper();
fileWrapper.id = contentDocument.Id;
fileWrapper.name = contentDocument.Title;
fileWrapper.fileExtension = contentDocument.FileExtension;
documentWrapper.files.add(fileWrapper);
}
documentsAndFiles.add(documentWrapper);
}
return documentsAndFiles;
}
}
// Trigger user who has access to documents records to have access to file records also
trigger ContentDocumentLinkTrigger on ContentDocumentLink (before insert) {
if (Trigger.isBefore && Trigger.isInsert) {
String lhwDocumentPrefix = Schema.getGlobalDescribe().get('Document__c').getDescribe().getKeyPrefix();
for (ContentDocumentLink cdl : Trigger.New) {
if (String.valueOf(cdl.LinkedEntityId).startsWithIgnoreCase(lhwDocumentPrefix)) {
cdl.Visibility = 'AllUsers';
}
}
}
}
Repository!
contains Custom Object, Apex Classes, Trigger and LWC Components
pgajek2/salesforce-download-file-in-community
If you have some questions go ahead and ask!
Resource
- https://help.salesforce.com/articleView?id=networks_customize_members.htm&type=5
- https://help.salesforce.com/articleView?id=000337432&type=1&mode=1
- https://developer.salesforce.com/docs/atlas.en-us.sfFieldRef.meta/sfFieldRef/salesforce_field_reference_ContentDocument.htm
- https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_contentdocument.htm
Was it helpful? Check out our other great articles here.
Hi,
Thanks for the great post. I followed your steps and when I try the Apex code, I am getting the below error in the FilesController.apxc :
Map<Id, Document__c> documents = new Map<Id, Document__c>([ SELECT Id, Name, Description__c FROM Document__c ]);
Invalid type: Document__c
Variable ‘documents’ doesn’t exist.
Please help with this.
Thanks
Finally !! Got a solution that worked !!
Thanks, Piotr
Hello I am not able to access ContentDistribution object in my org?
Hi, Great post!
I noticed every time ContentDistribution Object is created, an email is sent to the portal user who is viewing the file. Is it possible to turn this off?
Also, I noticed that the link that is given to download the file is always referring to the latest file version. Is there ways to download previous version as portal user?
Hi, We have similar requirements for a classic system, have followed all the above steps, but not able to access community user download link. Receiving the below error. URL No Longer Exists You have attempted to reach a URL that no longer exists on salesforce.com. URL: /sfc/servlet.shepherd/version/download/0680v000001Efn5 You may have reached this page after clicking on a direct link into the application. This direct link might be: • A bookmark to a particular page, such as a report or view • A link to a particular page in the Custom Links section of your Home Tab, or a Custom Link… Read more »
Greate post, I’m looking for this, but i have another problem with download multiple files as Zip by LinkedEntityId.
please help me.
Nice, this article saved me a lot of time, thank you Piotr.
Thank you for this post! I was looking for a suitable solution and in my opinion this one is one of the best possible.
Hi Piotr! Is there some limitation to different Community licenses? More precisely, it is about whether this solution will work for a regular Customer Community license (NOT Plus) where the documentation clearly says: Content is not available with Customer Community licenses.
Great topic, and exactly what I’m trying to do however I’m running into issues. In my community, the user takes some action and in apex code a .csv file is created and attached to a custom object to which the user has access. The user is also the file owner. When I generate the download URL as described above, the download redirects me to the first community in my list of communities in my scratch org, which basically says the community is down. I think this is default Salesforce behavior when something is not correct. Once I establish the url,… Read more »
Great post, Do you have entire source uploaded to github including front end?