How to download Salesforce Files in Community?

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

This image has an empty alt attribute; its file name is image-4-1024x217.png
Filles | Notes & Attachments

“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

This image has an empty alt attribute; its file name is image-5-1024x147.png
Share with ..

“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’.

This image has an empty alt attribute; its file name is image-1.png

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

  1. https://help.salesforce.com/articleView?id=networks_customize_members.htm&type=5
  2. https://help.salesforce.com/articleView?id=000337432&type=1&mode=1
  3. https://developer.salesforce.com/docs/atlas.en-us.sfFieldRef.meta/sfFieldRef/salesforce_field_reference_ContentDocument.htm
  4. 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.

5 11 votes
Article Rating
Subscribe
Notify of
guest
30 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Naincy
Naincy
7 months ago

Hello I am not able to access ContentDistribution object in my org?

Jessica
Jessica
7 months ago

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?

Dhiren
Dhiren
10 months ago

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 »

ldthach
ldthach
1 year ago

Greate post, I’m looking for this, but i have another problem with download multiple files as Zip by LinkedEntityId.
please help me.

Boris Gichev
1 year ago

Nice, this article saved me a lot of time, thank you Piotr.

Denys Yelchaninov
Denys Yelchaninov
1 year ago

Thank you for this post! I was looking for a suitable solution and in my opinion this one is one of the best possible.

Wojtek
1 year ago

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.

Vin
Vin
1 year ago

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 »

Korben
Korben
1 year ago

Great post, Do you have entire source uploaded to github including front end?

Close Menu
30
0
Would love your thoughts, please comment.x
()
x