Download multiple ContentVersion files as zip

In our org, there is a relatedList underneath the Contract object of ContentVersions of the contract and any amendments. Users wanted to be able to download all (or download selected) files with one click.

Such relatedList does not provide any download button. You need to select each file in turn, go to the Content page and click Download. Tedious. Users hated it. I hated it.

Googling revealed an interesting post on downloading attachments into a zip using JSZip. However, we had files greater than 15MB and I worried about Heap Limits.

Turns out SFDC provides download ContentVersion as zip already on the Content tab. (Where our users nor I almost never go as the Contract relatedList is more convenient). So, I used the browser developer Tools to see what URL was constructed by SFDC and saw that it exploited the undocumented servlet.shepherd.

The specific URL to use is:

{!URLFOR('/sfc/servlet.shepherd/version/download/' & delimitedSelectedIdList &'?')}

where delimitedSelectedIdList is a list of ContentVersion ids separated by forward slash. from my understanding, the ids separated by slashes is a sometimes-used REST convention to specify a list of resources.

Example (downloads 2 files into single zip)

{!URLFOR('/sfc/servlet.shepherd/version/download/068xxxxxxxxxxxxxxx/068yyyyyyyyyyyyyyy?')}

I tested this on large zip files (~400 MB) and had no issues.

Notes:

  1. servlet.shepherd is undocumented and hence unsupported so this might not be the right answer for a mission critical application.
  2. servlet.shepherd does not work on Attachments, only ContentVersion.

34 thoughts on “Download multiple ContentVersion files as zip

  1. Tasnim

    This is pretty cool… I have been using servlet.shepherd for downloading a single file from content. But didn’t know it could take in multiple content IDs and zip them all. Cool.

    Thanks a ton for sharing.

    Reply
  2. Anil Tilanthe

    Hello,
    Thanks a lot for the post.

    Is it possible to rename the Zip folder ?
    The Zip folder always has a name “downloaded-content”. So I need to change this name to “xyz.zip”.

    I also tried using JSZip and FileSaver (JS libraries). But I am unable to do it.

    Reply
    1. eric.kintzer@cropredy.com Post author

      Anil — I think I looked into this but as the URL is undocumented there was no obvious way I found to alter the name of the file. You could always try and add various URL params like &name=foo or &zip=foo but you’d be shooting in the dark here

      Reply
  3. AmBr

    It works ! ๐Ÿ™‚ Thanks
    But I’m also trying to rename the zip file. A lot a person are looking for a response on the web but nobody has the response

    Reply
    1. eric.kintzer@cropredy.com Post author

      Yes, this puzzled me for a while as there’s no documentation on this servlet. You might try various “guesses” with the URL parameters like ?name=foo.zip, ?filename=foo.zip, ?zip=foo.zip, … I really have no idea.

      Reply
      1. eric.kintzer@cropredy.com Post author

        Saurabh — as you can see from the comment thread, there is no known way to rename the zip file

        Reply
    1. eric.kintzer@cropredy.com Post author

      this would be a question for SFDC support or you can post on Salesforce Stackexchange

      Reply
  4. NBHK

    Hello,
    Thanks a lot.
    It works good but i’m facing a problem with files witch names contains special characters (รฉ รง…) . they are renamed as ‘0681l000000GHcn.pdf ‘ for example in the zip file.
    Any idea please

    Reply
    1. eric.kintzer@cropredy.com Post author

      As this is an undocumented interface from SFDC, your mileage may vary. I’d suggest applying a trigger to the uploaded files (i.e. ContentVersion) to coerce accented chars to a-zA-Z in filenames before they are saved.

      Reply
      1. Sagar

        @eric
        I have seen the same things if the file name has a Japanese character. Any idea? how to fix this with Japanese file name?

        Reply
    2. Admin Guy

      Files with special characters are not supported file names in Windows so that might be your issue. Not sure about IOS but I have definitely seen this in windows where windows strips special characters when downloading a file.

      Reply
  5. NBHK

    Hello, thanks for the post.
    It works good but i’m facing a problem with files witch names contains special characters (รฉ รง…) . they are renamed as 0681l000000GHcn.pdf for example in the zip file
    Any idea please

    Reply
  6. Raja

    Hi,

    /sfc/servlet.shepherd/version/download/068xxxxxxxxxxxxxxx

    This kind of url not working in Force.com Site pages. While downloading 401 Error coming. I have given read access to the objects also. Could you please help me on this.

    Reply
    1. eric.kintzer@cropredy.com Post author

      Raja

      There is a lengthy discussion of this here with the most helpful bits towards the end. I have not tried these techniques with Sites but good luck and let me know what you find.

      Reply
      1. Dagny Fernandes

        This will work but you need to append the site URL
        for Example:
        SFDC Normal:
        /sfc/servlet.shepherd/version/download/068xxxxxxxxxxxxxxx

        SFDC Site: (http://-sites.c24.force.com/TMS) – (This is site url)
        /TMS/sfc/servlet.shepherd/version/download/068xxxxxxxxxxxxxxx

        Reply
  7. Sai Patlolla

    Hi Eric,

    This is really great work and I admire it.

    I have custom page displaying multiple content document files. When user selects those files and clicks on send email (button on the page) then multiple files are sent in email. but i want to Zip these multiple files before sending email so that user will get one Zip files containing multiple files when they clicks on the send email button.

    Thanks

    Reply
    1. eric.kintzer@cropredy.com Post author

      Sai

      Zipping in Apex is not practical so the work has to be done at the client. I can imagine three options (none of which I have tried)

      Option 1 – reupload the zip to SFDC Files and then have the action of the custom page Email button add the zip as an attachment in the outbound Apex email. Be ware of Heap limits.

      Option 2 – Do all the work, including composing and sending the email from the client using the just downloaded zip

      Option3 – from the Apex side, use a third party service via callouts to do the zipping. Beware of Heap limits.

      Reply
  8. Khushi

    Hello,

    Thanks for the post its really workes.

    Can anyone help me to rename the zip document folder?
    I want to change the name and it is pretty urgent.

    Thanks

    Reply
    1. eric.kintzer@cropredy.com Post author

      this question has been asked many times and AFAIK, because the interface is undocumented, there is no known way to rename the zip file

      Reply
  9. Ronny

    Thanks for this great post ! helped us a lot !
    We got into one problem with performance.
    Once trying to zip many files (~100) with the total size of ~5GB it takes a LONG time to start downloading.. approx 20 min to START the download..
    Any ideas what can we do to speed things up?

    Reply
    1. eric.kintzer@cropredy.com Post author

      Ronny – if you go to the normal UI and multiselect the 100 files, and then click download – effectively this is the same servlet call I documented so the performance is going to be the same. I would try and selectively reduce the problem to see if it is one file in particular or the number of files. Try segmenting by file extension to see if it gives you a clue

      Reply
      1. Ronny

        Thanks for the quick reply.
        We are trying to expose these files in an “eCommerce” site we are building and external customers can’t wait that long.
        Do you have any other ideas how can we solve that?

        Reply
  10. Dinesh

    I need to have a button in lightning giving capability to download all the zip files but am not sure how to achieve this using the URLFOR function.

    Can someone elaborate more on this on how can this be achieved?

    Reply
    1. eric.kintzer@cropredy.com Post author

      Dinesh: Please post this question in Salesforce Stackexchange – this way the answer can be more broadly shared with the community

      Reply
  11. Riccardo Torchio

    Hello!
    I have this button to download files, but it’s not working in communities.
    I’ve tried to change the link from:
    “{!URLFOR(‘/sfc/servlet.shepherd/version/download/’ & vehIdspage & ‘?’)}”

    to

    “{!URLFOR(‘/sfsites/c/sfc/servlet.shepherd/version/download/’ & vehIdspage & ‘?’)}”

    but it’s not working.
    Can you give me some sort of advice please?

    PRESS HERE!

     

    ————————————-

    public with sharing class DocPerReferente_Controller {
    public String vehIdspage{get;set;}
    public Id posId {get;set;}

    public DocPerReferente_Controller(ApexPages.StandardController controller) {
    posId = controller.getRecord().Id;
    system.debug(‘ยงยงยงยง posId ‘+posId);
    String vehIds=”;
    List ListCon0 = new List();
    List ListCon1 = new List();
    try{
    ListCon1= [SELECT Id,ContentDocumentId ,LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId= :posId];
    ListCon0.addAll(ListCon1);
    }
    catch(Exception e){
    system.debug(‘ยงยงยงยง Errore a linea ‘+e.getLineNumber()+’: ‘+e.getMessage());
    }

    system.debug(‘ยงยงยงยง ListCon1 ‘+ListCon1);
    List ListCon5 = new List();
    List ListCon6 = new List();
    List SALList = new List();
    set MySopId= new set();
    set MyPratId= new set();
    set MyAttId= new set();
    set MySALId= new set();
    Pratica__c PP = [SELECT Id, Opportunity__c FROM PRatica__c WHERE Id=:posId];
    system.debug(‘ยงยงยงยง PP ‘+PP);
    try{
    List ListCon3= [SELECT Id,ContentDocumentId ,LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId =:posId];
    system.debug(‘ยงยงยงยง ListCon3 ‘+ListCon3);
    ListCon0.addAll(ListCon3);

    }
    catch(Exception e){
    system.debug(‘ยงยงยงยง Errore a linea ‘+e.getLineNumber()+’: ‘+e.getMessage());
    }

    //sopralluoghi
    List MySop = [Select Id, Opportunity__c FROM Sopralluogo__c WHERE Pratica__c=:posId];
    system.debug(‘ยงยงยงยง MySop ‘+MySop);
    for(Sopralluogo__c s:MySop){
    MySopId.add(s.Id);
    }
    //doc sopralluoghi
    if(MySopId.size()!=0){
    try{
    List ListCon2= [SELECT Id,ContentDocumentId ,LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId IN :MySopId];
    system.debug(‘ยงยงยงยง ListCon2 ‘+ListCon2);
    ListCon0.addAll(ListCon2);
    }
    catch(Exception e){
    system.debug(‘ยงยงยงยง Errore a linea ‘+e.getLineNumber()+’: ‘+e.getMessage());
    }
    }

    //Opty
    try{
    ListCon5 = [SELECT Id,ContentDocumentId ,LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId = :PP.Opportunity__c];
    system.debug(‘ยงยงยงยง ListCon5 ‘+ListCon5);
    ListCon0.addAll(ListCon5);
    }
    catch(Exception e){
    system.debug(‘ยงยงยงยง Errore a linea ‘+e.getLineNumber()+’: ‘+e.getMessage());
    }

    //Attivitร  Collaboratore
    try{
    List MyAtt = [Select Id, Pratica__c FROM Attivit_Collaboratore__c WHERE Pratica__c =:posId];
    system.debug(‘ยงยงยงยง MyAtt ‘+MyAtt);
    for(Attivit_Collaboratore__c p:MyAtt){
    MyAttId.add(p.Id);
    }
    system.debug(‘ยงยงยงยง MyAttId ‘+MyAttId);
    if(MyAttId.size()!=0){
    List ListCon4= [SELECT Id,ContentDocumentId ,LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId IN :MyAttId];
    ListCon0.addAll(ListCon4);
    }
    }
    catch(Exception e){
    system.debug(‘ยงยงยงยง Errore a linea ‘+e.getLineNumber()+’: ‘+e.getMessage());
    }

    //SAL
    try{
    SALList=[Select Id, Pratiche__c FROM SAL__c WHERE Pratiche__c =:posId];
    for(SAL__c s:SALList){
    MySALId.add(s.Id);
    }
    ListCon6= [SELECT Id,ContentDocumentId ,LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId IN :MySALId];
    ListCon0.addAll(ListCon6);
    }
    catch(Exception e){
    system.debug(‘ยงยงยงยง Errore a linea ‘+e.getLineNumber()+’: ‘+e.getMessage());
    }
    //interventi condomio?
    //interventi richiesti?
    //conviventi?
    //sal?
    system.debug(‘ยงยงยง ListCon0 ‘+ListCon0);
    system.debug(‘ยงยงยง ListCon0.size() ‘+ListCon0.size());
    for(ContentDocumentLink conDoc: ListCon0){
    for (ContentVersion docVersion : [Select Id, Disponibile_per_il_referente__c, VersionData from ContentVersion where ContentDocumentId =:conDoc.ContentDocumentId ]) {
    if(docVersion.Disponibile_per_il_referente__c==true){
    string ContentDown= docVersion.Id;
    vehIds+= ContentDown +’/’;
    }
    }
    vehIdspage=vehIds.removeEnd(‘/’);
    }

    }
    }

    Reply

Leave a Reply to Tasnim Cancel reply

Your email address will not be published. Required fields are marked *