Restrict File Count And File Size of Content Document For Any Salesforce Object

We often come to scenrio in business where we have to restrict file size and file count on any of the salesforce object. It can be any anything from Account, Contact, Opportunities or even on custom objects.

The solution was easier earlier while using Attachments as one single object was providing all details about files. But nowadays, things have become more challenging since salesforce has introduced new Content objects for lightning.

So here is the solution for those who are looking for it.

You can setup a custom setting to dynamically change the restrictions. Following are the steps to create custom setting in salesforce.

  1. Navigate to Setup > Custom Setting
  2. Click on new and select custom setting type as Hierarchy
  3. Name the custom setting as “File Limit Settings” and Object name as “File_Limit_Settings”
  4. Create 2 new custom fields of Type Number and name them as “Total File Count” and “File Size”. Remember, File Size field will always need file size limit in Kb. You can put this in description or help text to help users.
    File Size Limit Custom Setting Salesforce
  5. Add default organization level values in custom setting and save the record.
    Custom Setting default values salesforce

As salesforce have introduced new object structure for File Content objects, we need to write trigger on ContentDocumentLink standard object. You can find more details on this object structure in salesforce’s documents here : https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_erd_content.htm

Main objects that we come across in this apex implementation are ContentDocumentLink and ContentVersion. ContentDocumentLink object will help to figure out number of files related to any object’s record and ContentVersion object will give us the details about file size.

Following class will give you implementation details of file count validation and file size validation. Create new class in your salesforce org and copy following code. Modify values of parentObjectTypes list variable and add desired objects API name in the list for which you want to enforce the validation.

Apex Class

public class ContentDocumentLinkTriggerHandler{
    
    public static List parentObjectTypes = new List{'Account','Contact','My_Files__c'}; //parent object API names to use for validation
	
    public static void onAfterInsert(List lstNewDocumentLinks){
        
        //validate fiel size limits on all incoming documents
        List lstValidDocumentLinks = validateFileCount(lstNewDocumentLinks);
        
        //validate file size on Sobject for valid records
        if(!lstValidDocumentLinks.isEmpty())
            validateFileSize(lstValidDocumentLinks);
    }
    
    public static List validateFileCount(List lstNewDocumentLinks){
        
        Set setSobjectIds = new Set();
        List lstValidFiles = new List();
        
        Map> mapParentId_ContentDocumentLinkId = new Map>();
        Map mapChildId_ContentDocumentLinkId = new Map();
        
        for(ContentDocumentLink cdl :lstNewDocumentLinks){
            
            //get linked entity sobject type
            String objectType = getObjectType(cdl.LinkedEntityId);
            
            if(parentObjectTypes.contains(objectType)){
				
				if(!mapParentId_ContentDocumentLinkId.containsKey(cdl.LinkedEntityId))
					mapParentId_ContentDocumentLinkId.put(cdl.LinkedEntityId, new Set());
				
				mapParentId_ContentDocumentLinkId.get(cdl.LinkedEntityId).add(cdl.Id);
				setSobjectIds.add(cdl.LinkedEntityId);
            }
        }
        
        //calculate file count of each individual child and parent record
        if(!setSobjectIds.isEmpty()){
            
            //now query all content doc links related to sObjectIds
            List lstAllDocumentLinks = [SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId IN :setSobjectIds];
            
            //prepare list of all content documents related to parents
            Map> mapParentId_SetDocIds = new Map>();
            
            for(ContentDocumentLink cdl :lstAllDocumentLinks){
                
                if(!mapParentId_SetDocIds.containsKey(cdl.LinkedEntityId)){
                    
                    mapParentId_SetDocIds.put(cdl.LinkedEntityId, new Set());
                }
                
                mapParentId_SetDocIds.get(cdl.LinkedEntityId).add(cdl.ContentDocumentId); // map will be used to count documents related to each parents
            }
            
            if(!mapParentId_SetDocIds.isEmpty()){
                
                //validate file size for all parents
                Set invalidContentDocumentLinkIds = new Set();
                Map fileLimits = getFileLimits();
                Integer fileCountLimit = fileLimits.get('FileCount');
                    
				Decimal totalFiles = 0;
				
				for(Id parentId :setSobjectIds){
					
					if(mapParentId_SetDocIds.containsKey(parentId)){
						
						totalFiles = mapParentId_SetDocIds.get(parentId).size();
					}
					
					if(totalFiles > fileCountLimit && mapParentId_ContentDocumentLinkId.containsKey(parentId))
						invalidContentDocumentLinkIds.addAll(mapParentId_ContentDocumentLinkId.get(parentId));
				}
                
                if(!invalidContentDocumentLinkIds.isEmpty()){
                    
                    for(ContentDocumentLink cdl :lstNewDocumentLinks){
                        
                        if(invalidContentDocumentLinkIds.contains(cdl.Id)){
                            
                            cdl.addError('You can not upload more than ' + fileCountLimit + ' files.');
                        }
                        else{
                            lstValidFiles.add(cdl);
                        }
                    }
                }
                else{
                    
                    lstValidFiles.addAll(lstNewDocumentLinks);
                }
            }
        }
        
        return lstValidFiles;
    }
	
	public static void validateFileSize(List lstNewDocumentLinks){
        
        Set setSobjectIds = new Set();
        
        for(ContentDocumentLink cdl :lstNewDocumentLinks){
            
            //get linked entity sobject type
            String objectType = getObjectType(cdl.LinkedEntityId);
            
            if(parentObjectTypes.contains(objectType)){
                
                setSobjectIds.add(cdl.LinkedEntityId);
            }
        }
        
        if(!setSobjectIds.isEmpty()){
            
            //now query all content doc links related to sObjectIds
            List lstAllDocumentLinks = [SELECT ContentDocumentId, LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId IN :setSobjectIds];
            
            //prepare list of all content documents related to parents
            Map> mapParentId_SetDocIds = new Map>();
            Set setAllContentDocIds = new Set();
            
            for(ContentDocumentLink cdl :lstAllDocumentLinks){
                
                if(!mapParentId_SetDocIds.containsKey(cdl.LinkedEntityId)){
                    
                    mapParentId_SetDocIds.put(cdl.LinkedEntityId, new Set());
                }
                
                mapParentId_SetDocIds.get(cdl.LinkedEntityId).add(cdl.ContentDocumentId); // map will be used to count documents related to each parents
                setAllContentDocIds.add(cdl.ContentDocumentId); //this set will be used to query all content versions
            }
            
            if(!mapParentId_SetDocIds.isEmpty()){
                
				//query all content version records to file size information as Content Document Link object do not provide any file size info
                List lstContentVersions = [SELECT Id, ContentDocumentId, ContentSize FROM ContentVersion WHERE ContentDocumentId IN :setAllContentDocIds AND IsLatest = true];
                
                Map mapDocId_ContVersion = new Map();
                Set setInvalidDocIds = new Set();
                
                Map fileLimits = getFileLimits();
                Integer fileSizeLimit = fileLimits.get('FileSize'); // file size limit in Kb
                
                for(ContentVersion contVersion :lstContentVersions){
                    
                    Integer fileSizeInKb = Integer.valueOf(contVersion.ContentSize / 1024); //convert to Kb
                    
                    if(fileSizeInKb < fileSizeLimit)
                        mapDocId_ContVersion.put(contVersion.ContentDocumentId, contVersion);
                    else
                        setInvalidDocIds.add(contVersion.ContentDocumentId);
                }
                
                if(!setInvalidDocIds.isEmpty()){
                        
                    for(ContentDocumentLink cdl :lstNewDocumentLinks){
                    
                        if(setInvalidDocIds.contains(cdl.ContentDocumentId)){
                            
                            cdl.addError('You can not upload file exceeding ' + fileSizeLimit + 'Kb');
                        }
                    }
                }
            }
        }
    }
	
	public static Map getFileLimits(){
        
        Map mapFileLimits = new Map();
        
        //set default values
        mapFileLimits.put('FileCount', 10);
        mapFileLimits.put('FileSize', 10240); // file size 10 Mb in Kb
        
        File_Limit_Settings__c fileSetting = File_Limit_Settings__c.getOrgDefaults();
        
        //override default values from custom setting setup
        if(fileSetting != null){
            
            if(fileSetting.Total_File_Count__c != null)
                mapFileLimits.put('FileCount', Integer.valueOf(fileSetting.Total_File_Count__c));
            
            if(fileSetting.File_Size__c != null)
                mapFileLimits.put('FileSize', Integer.valueOf(fileSetting.File_Size__c));
        }
        
        return mapFileLimits;
    }
    
    public static String getObjectType(Id recordId){
        
        return recordId.getSObjectType().getDescribe().getName();
    }
}

After saving Handler class its time to create new trigger on ContentDocumentLink object using following code. Remember, we have to query related ContentDocument and ContentVersion information in logic, so your trigger needs to be created on After Insert event, we can not use Before Insert here.

Apex Trigger

trigger ContentDocumentLinkTrigger on ContentDocumentLink (after insert) {
    
    if(Trigger.isInsert)
        ContentDocumentLinkTriggerHandler.onAfterInsert(Trigger.New);
}

Your implementation will not be complete without Test class for code coverage right. So I am taking of this for you, just create another new apex class and copy below code in the class. Update any required field and parent object details in code, save the class and run the test.

Test Class

@isTest
public class ContentDocumentLinkTriggerHandlerTest{
    
    @isTest static void validateFileCountUpdate(){
        
		//bypass validation rules for test class
        INSERT new File_Limit_Settings__c(SetupOwnerId=UserInfo.getOrganizationId(),Total_File_Count__c = 1, File_Size__c = 100);
		
		//create custom object record
        Account testAccount = new Account(Name='testFileObject');
		INSERT testAccount;
		
		//create new content version
		ContentVersion contentVersion1 = new ContentVersion(
		  Title = 'Penguins',
		  PathOnClient = 'Penguins.jpg',
		  VersionData = Blob.valueOf('Test Content'),
		  IsMajorVersion = true
		);
		
		INSERT contentVersion1; 
		
		ContentVersion contentVersion2 = new ContentVersion(
		  Title = 'Penguins',
		  PathOnClient = 'Penguins.jpg',
		  VersionData = Blob.valueOf('Test Content'),
		  IsMajorVersion = true
		);
		
		INSERT contentVersion2; 
		
		//query document record
		List documents = [SELECT Id, Title, LatestPublishedVersionId FROM ContentDocument];

		//create ContentDocumentLink  record 
		ContentDocumentLink cdl1 = New ContentDocumentLink();
		cdl1.LinkedEntityId = testAccount.id;
		cdl1.ContentDocumentId = documents[0].Id;
		cdl1.shareType = 'V';
		INSERT cdl1;
		
		ContentDocumentLink cdl2 = New ContentDocumentLink();
		cdl2.LinkedEntityId = testAccount.id;
		cdl2.ContentDocumentId = documents[1].Id;
		cdl2.shareType = 'V';
		
		//this code will get an error because file size limit is set to 1 file
		try{
			INSERT cdl2;
		}
		catch(Exception e){
			//catch the exception as the trigger code is going to
			//throw an error for file size limit
			System.debug(e.getMessage());
		}
		
		system.assertEquals(null, cdl2.Id);
    }
}

Bonus Tip

You can use same code for storing file count and file size on any parent object field. Just make some minor twikings to save these details on parent sObject.

Post a comment here if you stuck at any point or have any query.