5

I am trying to download a JPG file stored as an attachment in Salesforce, to the local machine. I have a URL that works correctly when using a GET request in Workbench. When I try to do the same in Node.JS (using JSForce), I can't get the file to output as a JPG.

This is what I have:

var jsforce = require('jsforce');
var conn = new jsforce.Connection();
conn.login('<username>', '<password>', function(err, res) {
  if (err) { return console.error(err); }  
  conn.request(
    'https://<my-dev-ed-instance>.my.salesforce.com/services/data/v42.0/sobjects/ContentVersion/<uuid>/VersionData', 
    function(err, res) {
      if (err) { return console.error(err); }
      console.log(res);
      fs.writeFileSync('2.jpg',res,'base64');
    }
  );
});

I believe the GET request should be returning a BASE64 string, but I cannot seem to convert it or make it useful.

Any help would be greatly appreciated!

EDIT: I found a solution. Tbh, I really just needed some sleep and to wake up and use Postman to troubleshoot properly.

var https = require('follow-redirects').https;
var fs = require('fs');

var options = {
  'method': 'GET',
  'hostname': '<HOSTNAME>',
  'path': '/services/data/v49.0/sobjects/ContentVersion/<UUID>/VersionData',
  'headers': {
    'Authorization': 'Bearer <TOKEN>',
    'Cookie': 'BrowserId=<COOKIE>',
    'Content-Type': 'text/plain'
  },
  'maxRedirects': 20
};

var req = https.request(options, function (res) {
  var chunks = [];

  res.on("data", function (chunk) {
    chunks.push(chunk);
  });

  res.on("end", function (chunk) {
    var body = Buffer.concat(chunks);
    fs.writeFileSync('output.jpg',body,'binary');
  });

  res.on("error", function (error) {
    console.error(error);
  });
});

req.end();

Thank you to those who helped, you definitely pointed me in the right direction.

3 Answers 3

3

Here is a sample JS fetch request I converted in Postman (this is pseudocode and probably wont work). I use the the following endpoint /services/data/v48.0/sobjects/Attachment/<ATTACHMENT_FILE_ID>/Body BUT the response is binary so you would need to handle that response and convert it to JPG

https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_blob_retrieve.htm

You can also try getting file content with Connect API and the following endpoint (still binary response): /services/data/v{{version}}/connect/files/<FILE_ID>/content OR create a public file sharing link with the following endpoint and use that link one way or another (PUT request): /services/data/v{{version}}/connect/files/<FILE_ID>/file-shares/link

https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/connect_resources_files_content.htm

https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/connect_resources_files_shares_link.htm

var myHeaders = new Headers();
myHeaders.append("Authorization", "Bearer <ACCESS_TOKEN>");

var requestOptions = {
  method: 'GET',
  headers: myHeaders,
  redirect: 'follow'
};

fetch("https://domain.my.salesforce.com/services/data/v48.0/sobjects/Attachment/<ATTACHMENT_FILE_ID>/Body", requestOptions)
  .then(...)
1

The Blob Retrieve endpoint actually returns the file contents as a binary file. There's no need to base64 decode the value. See the documentation.

Attachment body content is returned in binary form. Note that the response content type will not be JSON or XML since the returned data is binary.

Note: this is for an Attachment Body, but also applies to any other Blob content.


Here's a quick example I wrote up:

var jsforce = require('jsforce');
var fs = require('fs');
const conn = new jsforce.Connection();
    
conn.login('<username>', '<password>').then(() => {
  conn.request(
    '/services/data/v49.0/sobjects/ContentVersion/.../VersionData'
  )
  .then((result) => {
    fs.writeFileSync('output.data', result);
  });
});
10
  • Unfortunately, I'm having trouble working out how to implement that in Node.JS, where I don't have access to cURL. Any ideas? Aug 23, 2020 at 10:29
  • @ScottHarrison I wrote up an example that runs on my computer. Give this a try and let me know.
    – sfdcfox
    Aug 23, 2020 at 10:56
  • Thank you for following up. Unfortunately, no go. It should be downloading a 130KB JPG file, so I changed output.data to output.jpg. What it downloads is a 235KB JPG that is unreadable. Aug 23, 2020 at 11:14
  • 1
    @ScottHarrison Sounds like you've got the wrong file. Even if it were base64 encoded, a 235kb base64 file would be about 176kb, not 130kb. Try opening the file in a hex editor or IDE, see what the contents looks like. It should start hex codes 'FF D8 FF' followed shortly by the text characters 'JFIF'. If not, you might have the wrong file.
    – sfdcfox
    Aug 23, 2020 at 11:29
  • 1
    I uploaded a totally different PNG file. Used Workbench to find the ID and VersionData URL. Used your sample code and had the exact same result. File was corrupted and with lots of padding to the file size. Funnily enough, a SOQL query in Workbench returns the binary string for VersionData, whilst in JSForce it returns that URL. Aug 23, 2020 at 11:57
0

The correct answer is to modify @sfdcfox/@Matthew Souther's answer with the critical, encoding: null property and object form of request:

var jsforce = require('jsforce');
var fs = require('fs');
const conn = new jsforce.Connection();
    
conn.login('<username>', '<password>').then(() => {
  conn.request({
    url: '/services/data/v49.0/sobjects/ContentVersion/.../VersionData',
    encoding: null
  })
  .then((result) => {
    fs.writeFileSync('output.data', result);
  });
});

Failure to do this results in the server sending the data in string form instead of binary.

Here is a real world working example that includes correct file name:

const result = await conn.sobject(`US_Order__c`) // Custom table
    .select(`*, US_Order__c.*`)
    .include(`AttachedContentDocuments`) // There are other attachments
      .select(`*`)
      .orderby(`CreatedDate`, `DESC`)
      .end()
    .where(`Name = 'O-00012085'`)
    .execute();

for ( const record of result ) {
  if ( record.AttachedContentDocuments ) {
    for ( const attachment of record.AttachedContentDocuments.records ) {
      const result = await conn.sobject(`ContentVersion`)
        .select(`*, ContentVersion.*`)
        .where(`ContentDocumentId = '${attachment.ContentDocumentId}'`)
        .execute();

      for ( const version of result ) {
        const result = await conn.request({ url: version.VersionData, encoding: null });

        fs.writeFileSync(path.resolve(__dirname + `/${version.PathOnClient}`), result);
      }
    }
  }
}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .