Upload content to File Attribute in Dynamics 365/ CDS from JavaScript

Follow my blog for more interesting topics on Dynamics 365, Portals and Power Platform. For training and consulting, write to us at info@xrmforyou.com


This has been a long ask from my readers since the time I wrote the blog on how to read contents of File attribute using JavaScript in Dynamics 365. So here I am back with another blog to explain on how we can upload content to a File Attribute using JavaScript.


Now before I go ahead and try to do the same, let’s understand the scenarios where this can be beneficial. If you are developing a PCF control where you need to upload data to file attribute, this can be beneficial. Also you may need to do it from webresource or even on click of ribbon button.
So let’s get started. As surprising it is, the way to upload the file content can be quite time consuming. You don’t have ready made code in Microsoft Docs to do the same. Neither the instruction is quite detailed to help you construct the code for file upload. So before I put the code, a quick walkthrough of the steps.

  • The first step is to go ahead and upload an empty file. In the first request we should not pass any content. Rather the first request is only to get the session token which should be used for all consecutive upload requests. This is very similar to the way you upload a large file in SharePoint in chunks. If everything is fine, once the request is completed, the status would be 200.
  • For all consecutive requests, we include the session token (also called FileContinuationToken) till the file upload is complete. The process is in place to upload file in chunks instead of big bang approach which will fail for most of scenarios where we handle large file. Once each request is completed, the request status would be 206 (Partial content) and when the last byte is uploaded, the request status will be 204. One more important point is the Content-Range header for all consecutive requests.
  • A data file of 16 MB or less can be accomplished in a single API call while uploading more than 16 MB of data requires the file data to be divided into blocks of 4 MB or less data.

So let’s finally jump in to the code.
the source
For this simple example, I have used the HTML file upload control to upload content to the File attribute.
Attribute Namenew_filecontent
Entity Nameaccount

function makeRequest(method, fileName, url, bytes, firstRequest, offset, count, fileBytes) {
    return new Promise(function (resolve, reject) {
       var request = new XMLHttpRequest();
       request.open(method, url);
      if (firstRequest)
          request.setRequestHeader("x-ms-transfer-mode", "chunked");
      request.setRequestHeader("x-ms-file-name", fileName);
      if (!firstRequest) {
          request.setRequestHeader("Content-Range", "bytes " + offset + "-" + (offset + count - 1) + "/" + fileBytes.length);
          request.setRequestHeader("Content-Type", "application/octet-stream");
       }
      request.onload = resolve;
       request.onerror = reject;
      if (!firstRequest)
          request.send(bytes);
       else
          request.send();
    });
}
function uploadFile() {
    var fileControl = document.getElementById("file_input");
    var reader = new FileReader();
    var fileName = fileControl.files[0].name;
    var entitySetName = "accounts"; // change entity as per your requirement.
    var recordId = "98c9232e-e4c5-ea11-a812-000d3af2f505"; // record guid.
    reader.onload = function () {
      var arrayBuffer = this.result;
       array = new Uint8Array(arrayBuffer);
      var url = parent.Xrm.Utility.getGlobalContext().getClientUrl() + "/api/data/v9.1/" + entitySetName + "(" + recordId + ")/new_filecontent";
      // this is the first request. We are passing content as null.
       makeRequest("PATCH", fileName, url, null, true).then(function (s) {
          fileChunckUpload(s, fileName, array);
       });
    };
   reader.readAsArrayBuffer(fileControl.files[0]);
}
async function fileChunckUpload(response, fileName, fileBytes) { 
   var req = response.target;
    var url = req.getResponseHeader("location");
    var chunkSize = parseInt(req.getResponseHeader("x-ms-chunk-size"));
    var offset = 0;
    while (offset <= fileBytes.length) {
       var count = (offset + chunkSize) > fileBytes.length ? fileBytes.length % chunkSize : chunkSize;
       var content = new Uint8Array(count);
       for (var i = 0; i < count; i++) {
          content[i] = fileBytes[offset + i]; 
      } 
      response = await makeRequest("PATCH", fileName, url, content, false, offset, count, fileBytes);
       req = response.target;
       if (req.status === 206) { // partial content, so please continue. 
         offset += chunkSize;
       }
       else if (req.status === 204) { // request complete.
          break;
       }
       else { // error happened.
          // log error and take necessary action. 
         break;
       }
    }
}

Observe how I have made the first request and post the first request I am using the location response header to get the URL which returns the URL with file continuation token. Also note how I have passed different request headers for first time request and consecutive request.


And once you plugin this code, you are now ready to upload file of any size which is allowed as per your file attribute size.
Hope this helps!
Debajit Dutta
(Business Solutions MVP)