Power Pages: Upload large files – work in progress

Olena GrischenkoPower Automate5 months ago23 Views

On the project, we need our portal users to be able to upload large video files. It’s required for incident investigations.

As we all know, the large file upload was one of the top-asked features. So now we could finally use it with Power Pages. Yay!

No code

The official documentation is here:

https://learn.microsoft.com/en-us/power-platform/release-plan/2024wave1/power-pages/upload-files-up-10-gb-azure-blob-storage-power-pages

The amazing Nick Doelman’s video with the step-by-step set-up instructions is here:

Thank you very much, Nick! Your video saved me lots of time!

So, I got it all working but unfortunately, it doesn’t fully cover our end-to-end process at the moment.

Let’s say, I need to implement these two scenarios, ideally working OOTB without coding.

Scenario 1: A Power Pages user can upload large files from a portal to Azure BLOB.

Scenario 2: A Dynamics 365 user can download the file from Azure BLOB to review video footage.

Scenario 1 works fine. However, the code for Scenario 2 is not available at the moment, not for a new, enhanced data model.

It used to work like this: Add Azure storage web resource to a form | Microsoft Learn

What’s not working now? Personally, I couldn’t find the web resource mentioned in the article above in a new data model. Honestly, the HTML resource sounds a bit outdated anyway. So I hope a new control will be released very soon.

I think I will just go with a custom code for the Dynamics 365 file download.

But first I will try to copy the page from the old website to see if anything from the old code could be repurposed. Maybe you will see it in the next article 😉

Web API

Even with Scenario 1 we still need to code as we use a Document Details custom table to store the uploaded file information. We have to populate the details during the upload – a file name, a type, a size and a location on the Document Details record. Also, we upload files to SharePoint and Azure storage both. To create a nice user experience we probably need to code.

Fortunately, Web API is available for us!

https://learn.microsoft.com/en-us/power-pages/configure/webapi-azure-blob

First, I was determined to get it all working by following the instruction in the article but I gave up quickly. The approach taken in the article is a little bit like

WOW! IT’S TOO MUCH!

By following the steps we are trying to get fetch, upload, download and delete working – all at once! Debugging the code if something isn’t working (never happened to us before) would be a nightmare.

For now, I only need a part where a user uploads a file. Let’s focus on uploading first!

Upload file

Follow the article to Step 3. Make sure the table you use to attach the file is covered by Table permissions. Plus child permission for Notes. I use a custom Case table (tema_case).

I also found I need to enable Web API for my custom table as I was getting an error like this:

{
  "error": {
    "code": "90040101",
    "message": "Attribute tema_caseid in table tema_case is not enabled for Web Api."
  }
}

So I added this to the Site Settings:

Site setting name Value
Webapi/tema_case/enabled true
Webapi/tema_case/fields *

As I let a user to upload files from the Updae Case form, I use a Basic form Additional Settings to add the JavaScript code:

Now, let’s add the upload file control to the form. As it’s only a POC, I added it to the end of the panel like this:

$(document).ready(function (){
    $("#EntityFormPanel").append( "<input type="file" id='fileinput' name="fileinput" onchange="uploadFileinChunks();" />" );   
}); 

Looks like this on the page:

Here I used Microsoft code from the article and just cleaned it a bit to only leave the bits which are absolutely required. It’s easier to debug and also understand.

When the file is selected we call the uploadFileinChunks() function. Getting the entityName and Id, constructing the Web API URL. Getting the file name and size from the upload file control we added to the form.

function uploadFileinChunks()
{    
    var entityName = "tema_case";//replace with yours
    var entityId = window.location.search.substring(4);
    
    var url = "/_api/file/InitializeUpload/" + entityName + "(" + entityId + ")/blob"
    var elementToChooseFile = document.getElementById("fileinput");
    var filename = "";
    if (elementToChooseFile.files.length > 0) {
        filename = elementToChooseFile.files[0].name;
        filesizeelement = elementToChooseFile.files[0].size / 1048576;      
        const encodedFileName = encodeURIComponent(filename);
        filename = encodedFileName;

Checking if we have the file or displaying an alert “No file chosen.”

We upload the file in chunks. As it’s large! 🙂

If we have the file then we try to figure out a number of blocks for the upload.

if (elementToChooseFile.files.length > 0 && elementToChooseFile.files[0].size > 0)
        {
            const chunkSize = 50*1024 *1024;
            let numberOfBlocks;
            let token;
            if (elementToChooseFile.files[0].size % chunkSize == 0)
            {
                numberOfBlocks = elementToChooseFile.files[0].size / chunkSize;
            }
            else
            {
                numberOfBlocks = parseInt(elementToChooseFile.files[0].size / chunkSize, 10) + 1;
            }
            //TO BE ADDED
            
         }
        else{
            alert("No file chosen.");
        }
    }

We need the actual safeAjax function to take care of the upload:

(function(webapi, $){
                function safeAjax(ajaxOptions) {
                    var deferredAjax = $.Deferred();

                    shell.getTokenDeferred().done(function (token) {
                    // add headers for AJAX
                    if (!ajaxOptions.headers) {
                    $.extend(ajaxOptions, {
                        headers: {
                            "__RequestVerificationToken": token
                        }
                    }); 
                    } else {
                    ajaxOptions.headers["__RequestVerificationToken"] = token;
                }
                $.ajax(ajaxOptions)
                    .done(function(data, textStatus, jqXHR) {
                        validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
                    }).fail(deferredAjax.reject); //AJAX
            }).fail(function () {
                deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
            });

            return deferredAjax.promise();  
        }
        webapi.safeAjax = safeAjax;
    })(window.webapi = window.webapi || {}, jQuery)

Now, the fun part, calling the function:

 webapi.safeAjax({
                type: "POST",
                url: url,
                headers: { "x-ms-file-name": elementToChooseFile.files[0].name, "x-ms-file-size": elementToChooseFile.files[0].size },
                contentType: "application/octet-stream",
                processData: false,
                data: {},
                success: function (response, status, xhr)
                {
                    token = response;                   
                    uploadFileChunk(0);
                },
                error: function (XMLHttpRequest, textStatus, errorThrown)
                {
                    alert(XMLHttpRequest.responseText);
                }
            });

Here we call the uploadFileChunk function recursively, incrementing the blockno until the upload is completed.

function uploadFileChunk(blockno)
{
    var fileReader = new FileReader();

    if (blockno < numberOfBlocks)
    {
        var end = (blockno * chunkSize + chunkSize) > elementToChooseFile.files[0].size ? blockno * chunkSize + elementToChooseFile.files[0].size % chunkSize : blockno * chunkSize + chunkSize;
        var content = elementToChooseFile.files[0].slice(blockno * chunkSize, end);
        fileReader.readAsArrayBuffer(content);
    }
    fileReader.onload = function ()
    {
        webapi.safeAjax({
            type: "PUT",
            url: "/_api/file/UploadBlock/blob?offset=" + (blockno * chunkSize) + "&fileSize=" + elementToChooseFile.files[0].size + "&chunkSize=" + chunkSize + "&token=" + token,
            headers: { "x-ms-file-name": elementToChooseFile.files[0].name },
            contentType: "application/octet-stream",
            processData: false,
            data: content,
            success: function (res) {                            
                var percentComplete = ((parseFloat(end) / parseFloat(elementToChooseFile.files[0].size)) * 100).toFixed(2);
                alert("Completed: "+percentComplete);                               
                if (percentComplete == 100) {
                   alert("File upload is completed.");                                  
                }
                uploadFileChunk(blockno + 1);
            },
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                alert(XMLHttpRequest.responseText);
            }
        });
    }
}

As I said, it doesn’t have a progress bar, it doesn’t display the file info. I used alerts to display the information of the % of the file load.

The whole block of code is here (replace double quotes with the correct characters) :

$(document).ready(function (){
    $("#EntityFormPanel").append("<input type="file" id='fileinput' name="fileinput" onchange="uploadFileinChunks();" />" );   
}); 

(function(webapi, $){
                function safeAjax(ajaxOptions) {
                    var deferredAjax = $.Deferred();

                    shell.getTokenDeferred().done(function (token) {
                    // add headers for AJAX
                    if (!ajaxOptions.headers) {
                    $.extend(ajaxOptions, {
                        headers: {
                            "__RequestVerificationToken": token
                        }
                    }); 
                    } else {
                    ajaxOptions.headers["__RequestVerificationToken"] = token;
                }
                $.ajax(ajaxOptions)
                    .done(function(data, textStatus, jqXHR) {
                        validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
                    }).fail(deferredAjax.reject); //AJAX
            }).fail(function () {
                deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
            });

            return deferredAjax.promise();  
        }
        webapi.safeAjax = safeAjax;
    })(window.webapi = window.webapi || {}, jQuery)

function uploadFileinChunks()
{    
    var entityName = "tema_case";
    var entityId = window.location.search.substring(4);
    
    var url = "/_api/file/InitializeUpload/" + entityName + "(" + entityId + ")/blob"
    var elementToChooseFile = document.getElementById("fileinput");
    var filename = "";
    if (elementToChooseFile.files.length > 0) {
        filename = elementToChooseFile.files[0].name;
        filesizeelement = elementToChooseFile.files[0].size / 1048576;      
        const encodedFileName = encodeURIComponent(filename);
        filename = encodedFileName;

        if (elementToChooseFile.files.length > 0 && elementToChooseFile.files[0].size > 0)
        {
            const chunkSize = 50*1024 *1024;
            let numberOfBlocks;
            let token;
            if (elementToChooseFile.files[0].size % chunkSize == 0)
            {
                numberOfBlocks = elementToChooseFile.files[0].size / chunkSize;
            }
            else
            {
                numberOfBlocks = parseInt(elementToChooseFile.files[0].size / chunkSize, 10) + 1;
            }

            webapi.safeAjax({
                type: "POST",
                url: url,
                headers: { "x-ms-file-name": elementToChooseFile.files[0].name, "x-ms-file-size": elementToChooseFile.files[0].size },
                contentType: "application/octet-stream",
                processData: false,
                data: {},
                success: function (response, status, xhr)
                {
                    token = response;                   
                    uploadFileChunk(0);
                },
                error: function (XMLHttpRequest, textStatus, errorThrown)
                {
                    alert(XMLHttpRequest.responseText);
                }
            });
            function uploadFileChunk(blockno)
            {
                var fileReader = new FileReader();

                if (blockno < numberOfBlocks)
                {
                    var end = (blockno * chunkSize + chunkSize) > elementToChooseFile.files[0].size ? blockno * chunkSize + elementToChooseFile.files[0].size % chunkSize : blockno * chunkSize + chunkSize;
                    var content = elementToChooseFile.files[0].slice(blockno * chunkSize, end);
                    fileReader.readAsArrayBuffer(content);
                }
                fileReader.onload = function ()
                {
                    webapi.safeAjax({
                        type: "PUT",
                        url: "/_api/file/UploadBlock/blob?offset=" + (blockno * chunkSize) + "&fileSize=" + elementToChooseFile.files[0].size + "&chunkSize=" + chunkSize + "&token=" + token,
                        headers: { "x-ms-file-name": elementToChooseFile.files[0].name },
                        contentType: "application/octet-stream",
                        processData: false,
                        data: content,
                        success: function (res) {                            
                            var percentComplete = ((parseFloat(end) / parseFloat(elementToChooseFile.files[0].size)) * 100).toFixed(2);
                            alert("Completed:"+percentComplete);                               
                            if (percentComplete == 100) {
                               alert("File upload is completed.");                                  
                            }
                            uploadFileChunk(blockno + 1);
                        },
                        error: function (XMLHttpRequest, textStatus, errorThrown) {
                            alert(XMLHttpRequest.responseText);
                        }
                    });
                }
            }
         }
        else{
            alert("No file chosen.");
        }
    }
    
}

Now as we got the basics working we could start adding the additional features.

You could also try the additional functions from the article. Adding it one by one.

File info in Dynamics 365 attached to the Case record as a txt file:

And in Azure storage container:

Hope it all makes sense.

Original Post https://msolenacrm.blog/2024/09/08/power-pages-upload-large-files-work-in-progress/

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Join Us
  • X Network2.1K
  • LinkedIn3.8k
  • Bluesky0.5K
Support The Site
Events
March 2025
MTWTFSS
      1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31       
« Feb   Apr »
Follow
Sign In/Sign Up Sidebar Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...