Here’s a sample of the end result:
Why do this?
You may be asking why upload using the API when we can now use a basic form or multistep form to upload to file columns?
One reason is with forms, it’s only only possible in Edit mode, meaning the record must already exist. This can make it a more clunky, click-heavy process than it needs to be.
Plus, the web API is all about opening up new UI / UX possibilities and breaking free from forms. Once you’ve mastered these techniques, the user interface / user experience is totally up to you. Maybe you want a drag and drop experience that creates child records… Maybe you want to use them in a PCF control… so many possibilities 😎
Introduction
As if creating an amazing tool wasn’t enough, Guido went out of his way to help me in my exploration around uploading to Image and File columns in the Portal Web API even before his tool specifically supported this in the world of portals 🙏 🙇♂️
If you’re already familiar with uploading images to the Portal Web API (see my post here), you’ll find many similarities here. The key difference is that with images, we read and transferred the content in base64 format whereas for File columns, we’ll read and transfer the file contents as a byte array.
Here are the high-level steps involved with uploading to a File column to a new record:
- Authenticate with the Portal Web API
- Provide a form input to select a file
- Get the name of the selected file
- Get the contents of the selected file (in binary format) using the readAsArrayBuffer API
- Create the record using the portal web API
- Upload the file to the newly created record using Portal Web API
Prerequisites
- Create a File column in your Dataverse / Dynamics instance
- Set up the necessary Table Permissions and Site Settings to allow use of the Portal Web API with the required table and File column (I use another awesome XRMToolBox application for this, Power Portal Web API Helper by the ever helpful and super knowledgeable Omar Zaarour)
- Install XRMToolBox
- Install the Dataverse REST Builder tool
- Connect to your environment
Step 1 – Add a form allowing the user to select a file
My form contains a text input for the track name and a file input that accepts a single MP3 file.
Planning ahead, I’ve added a hidden div and an audio player so that the user can preview their audio upon successful upload
<div id="upload-Audio"> <h1>Create Track</h1> <form id="track-content"> <label for="track-name">Track Name</label> <input id="track-name" type="text"> <input id="file-upload" accept="audio/mpeg,audio/mpeg3,audio/x-mpeg-3,audio/ogg" type="file"> <a class="btn btn-default" id="submitFile" style="display:none">Upload</a> </form> <!-- This div and audio player is hidden by default but will be shown if the upload succeeds --> <div class="showOnSuccess" role="alert" style="display:none; margin-top: 1em;"> <audio id="upload-player" controls> <source type="audio/mpeg">Your browser does not support the audio element. </audio> </div> </div>
Step 2 – Listen out for a file selection and perform some checks
First we need an event listener. In this example, I’ll listen for a change in the file upload field.
I wouldn’t want to upload a track without a name, nor to allow upload of the wrong file type (for security reasons as well as usability)… so I’ve added checks to ensure that these are populated.
function createFile() { var record = {}; // Get the name from the form field and escape any special characters record.musdyn_name = encodeURIComponent(trackName); $('#upload-Audio h1').text('Uploading...'); webapi.safeAjax({ type: "POST", url: "/_api/musdyn_tracks", contentType:"application/json", data: JSON.stringify(record), success: function (data, textStatus, xhr) { // If record creation succeeds, get the GUID of the newly created record and store in a varibale for easy reuse var entityID = xhr.getResponseHeader("entityid"); // Provide some visual feedback re success of this stage $('#upload-Audio h1').text('Track created, uploading audio file'); // Run the getFileContentsAndMetadata function, passing the GUID of the newly created record getFileContentsAndMetadata(entityID); }, error: function (xhr, textStatus, errorThrown) { // If the creation fails, provide some visual feedback $('#upload-Audio h1').text('Uh oh, error creating the track'); } }); }
Step 3 – Create the Track record using the portal Web API
Note – before you make any API calls, you’ll need to include the API Wrapper AJAX function to verify the source. Don’t worry, this is as easy as copying a snippet. Follow my quick guide here
Generating our API call using Dataverse REST Builder
Launch XRMToolBox and connect to your environment
Open Dataverse REST Builder from the list of tools
Go to File > New Collection
Click on New Request
Set the Request Type to Manage File Data
In the Configure tab, set the following options as required:
Open the Portals tab. There’s our code, helpfully written for us 🙂
All the heavy listing here is done by Dataverse REST Builder. All we need to do is:
- Wrap Guido’s code in a function so it can be called on demand
- Replace the hard-coded name with our variable
- Decide what we want to do on success completion, and on error
Here’s the format of a function:
function nameOfFunction() { // The code goes here }
We’ll be ‘chaining’ a number of functions together. If the first succeeds, take some information and fire the next. If that succeeds, take some information and fire the next
In this example, that’ll be:
- Run the createRecord function
- If that succeeds, run the getFileContentsAndMetadata function, passing it the GUID of our newly created record
- If that succeeds, run the uploadFile function, passing it the file contents, metadata and our GUID
…plus we’ll be providing visual feedback along the way about progress / success / any errors encountered
function createFile() { var record = {}; // Get the name from the form field and escape any special characters record.musdyn_name = encodeURIComponent(trackName); $('#upload-Audio h1').text('Uploading...'); webapi.safeAjax({ type: "POST", url: "/_api/musdyn_tracks", contentType:"application/json", data: JSON.stringify(record), success: function (data, textStatus, xhr) { // If record creation succeeds, get the GUID of the newly created record and store in a varibale for easy reuse var entityID = xhr.getResponseHeader("entityid"); // Provide some visual feedback re success of this stage $('#upload-Audio h1').text('Track created, uploading audio file'); // Run the getFileContentsAndMetadata function, passing the GUID of the newly created record getFileContentsAndMetadata(entityID); }, error: function (xhr, textStatus, errorThrown) { // If the creation fails, provide some visual feedback $('#upload-Audio h1').text('Uh oh, error creating the track'); } }); }
Step 4 – Get the contents and the name of the selected file
At this stage, although there is a file selected, the contents aren’t yet available in the browser and nothing has been uploaded. We need to tell the browser to read the file and make its contents available before we can send anything with the API.
If there is a filename and a file in the correct format, run the uploadFile function, passing it the file content, file name and the GUID of the newly created record
function getFileContentsAndMetadata(entityID) { // Get the name of the selected file var fileName = encodeURIComponent(document.getElementById('file-upload').files[0].name); // Get the content of the selected file var file = document.getElementById('file-upload').files[0]; // If the user has selected a file if (file) { // Read the file as a byte array var reader = new FileReader(); reader.readAsArrayBuffer(file); // The browser has finished reading the file, we can now do something with it... reader.onload = function(e) { // The browser has finished reading the file, we can now do something with it... var fileContent = e.target.result; // Run the function to upload to the Portal Web API, passing the GUID of the newly created record and the file's contents and name as inputs uploadFile(fileContent,fileName,entityID); }; } }
Step 5 – Upload to the Portal Web API
In this example, I use PUT as I only needed to update a single column. Otherwise, we’d use PATCH (whether that’s to create a record or to update multiple columns for an existing record).
Dataverse REST Builder has done all the heavy lifting for us. So let’s add the finishing touches. All we need to do is:
- Wrap this code in a function, allowing us to call it on demand
- Swap the hard-coded GUID, file name and file contents with our variables
- (Optional) Design what to do on success / error (provide some visual feedback at least)
Building the file upload API call using Dataverse REST Builder
Go to File > New Collection
Click on New Request
Set the Request Type to Manage File Data
In the Configure tab, set the following options as required:
Open the Portals tab
Copy and paste the code into a function called uploadFile
Guido does all kinds of clever stuff for us just in case the file contents aren’t already in a suitable format (e.g. if we needed to convert form base64). Luckily, ours will be in the wight format do most of what we’re doing is nice and straight forward.
We can remove all the file conversion stuff
Now all we need to do is replace the hardcoded file name and file contents with our variables
// Upload the file to the newly created Track record function uploadFile(fileContent,fileName,entityID) { var record = {}; record.musdyn_name = fileName; webapi.safeAjax({ type: "PUT", // NOTE: right now Portals requires PUT instead of PATCH for the upload url: "/_api/musdyn_tracks(" + entityID + ")/musdyn_audiotrack?x-ms-file-name=" + fileName, contentType: "application/octet-stream", data: fileContent, processData: false, success: function (data, textStatus, xhr) { // Provide some visual feedback re successful upload $('#upload-Audio h1').html('<span style="color: #1ed760;" class="glyphicon glyphicon-ok"></span> Here\'s your audio'); // Set the source of the hidden audio player audioPlayerSrcURL = '/File/download.aspx?Entity=musdyn_track' + '&Attribute=musdyn_audiotrack&Id=' + entityID; $('#upload-player').attr('src',audioPlayerSrcURL) // Show the audio player $('.showOnSuccess').slideDown(200); // Hide the form $('#track-content').slideUp(200); }, error: function (xhr, textStatus, errorThrown) { // If error occurs uploading the file, provide visual feedback $('#upload-Audio h1').text('Uh oh, uploading file the track'); } }); }
The visual feedback – where does the audio file come from?
In my example, I’m uploading an MP3 file. On success, I figured it would be reassuring to allow the user to play the file they just uploaded.
If the upload fails (the ajax returns an error), I’ll show a basic error message on screen and log a more detailed error in the console.
All we need to access our file is a download URL in the following format. It needs 3 pieces of information from us:
- The logical name of the table
- The logical name of the column
- The GUID of the record
Here’s an example:
https://songify.powerappsportals.com/File/download.aspx?Entity=musdyn_track&Attribute=musdyn_audiotrack&Id=c9013cdf-3d35-eb11-8fed-281878c98667
…and here’s how that looks using my entityID variable (no use hard-coding the GUID!!) and setting the source of the audio player with jQuery:
audioPlayerSrcURL = '/File/download.aspx?Entity=musdyn_track' + '&Attribute=musdyn_audiotrack&Id=' + entityID; $('#upload-player').attr('src',audioPlayerSrcURL);
The full web template
{% include 'Portal Web API Wrapper' %} <div id="upload-Audio"> <h1>Create Track</h1> <form id="track-content"> <label for="track-name">Track Name</label> <input id="track-name" type="text"> <input id="file-upload" accept="audio/mpeg,audio/mpeg3,audio/x-mpeg-3,audio/ogg" type="file"> <a class="btn btn-default" id="submitFile" style="display:none">Upload</a> </form> <!-- This div and audio player is hidden by default but will be shown if the upload succeeds --> <div class="showOnSuccess" role="alert" style="display:none; margin-top: 1em;"> <audio id="upload-player" controls> <source type="audio/mpeg">Your browser does not support the audio element. </audio> </div> </div> <script> var trackName; // on change to the file upload form field, check we have the required inputs and run the createFile function $( "#file-upload" ).on( "change", function() { trackName = document.getElementById('track-name').value; //selectedFile = $( "#file-upload" ).val(); var fileMimeType = document.getElementById('file-upload').files[0].type; alert(fileMimeType); if(trackName != '') { // Check this is an audio file if(fileMimeType.startsWith("audio/")) { createFile(); } else { alert("This doesn't appear to be an audio file"); } } }); function createFile() { var record = {}; // Get the name from the form field and escape any special characters record.musdyn_name = encodeURIComponent(trackName); $('#upload-Audio h1').text('Uploading...'); webapi.safeAjax({ type: "POST", url: "/_api/musdyn_tracks", contentType:"application/json", data: JSON.stringify(record), success: function (data, textStatus, xhr) { // If record creation succeeds, get the GUID of the newly created record and store in a varibale for easy reuse var entityID = xhr.getResponseHeader("entityid"); // Provide some visual feedback re success of this stage $('#upload-Audio h1').text('Track created, uploading audio file'); // Run the getFileContentsAndMetadata function, passing the GUID of the newly created record getFileContentsAndMetadata(entityID); }, error: function (xhr, textStatus, errorThrown) { // If the creation fails, provide some visual feedback $('#upload-Audio h1').text('Uh oh, error creating the track'); } }); } function getFileContentsAndMetadata(entityID) { // Get the name of the selected file var fileName = encodeURIComponent(document.getElementById('file-upload').files[0].name); // Get the content of the selected file var file = document.getElementById('file-upload').files[0]; // If the user has selected a file if (file) { // Read the file as a byte array var reader = new FileReader(); reader.readAsArrayBuffer(file); // The browser has finished reading the file, we can now do something with it... reader.onload = function(e) { // The browser has finished reading the file, we can now do something with it... var fileContent = e.target.result; // Run the function to upload to the Portal Web API, passing the GUID of the newly created record and the file's contents and name as inputs uploadFile(fileContent,fileName,entityID); }; } } // Upload the file to function uploadFile(fileContent,fileName,entityID) { var record = {}; record.musdyn_name = fileName; webapi.safeAjax({ type: "PUT", // NOTE: right now Portals requires PUT instead of PATCH for the upload url: "/_api/musdyn_tracks(" + entityID + ")/musdyn_audiotrack?x-ms-file-name=" + fileName, contentType: "application/octet-stream", data: fileContent, processData: false, success: function (data, textStatus, xhr) { // Provide some visual feedback re successful upload $('#upload-Audio h1').html('<span style="color: #1ed760;" class="glyphicon glyphicon-ok"></span> Here\'s your audio'); // Set the source of the hidden audio player audioPlayerSrcURL = '/File/download.aspx?Entity=musdyn_track' + '&Attribute=musdyn_audiotrack&Id=' + entityID; $('#upload-player').attr('src',audioPlayerSrcURL) // Show the audio player $('.showOnSuccess').slideDown(200); // Hide the form $('#track-content').slideUp(200); }, error: function (xhr, textStatus, errorThrown) { // If error occurs uploading the file, provide visual feedback $('#upload-Audio h1').text('Uh oh, uploading file the track'); } }); } </script>