Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I upload a file from the user's browser directly to Azure Blob Storage using a Blazor Server application?

I am hosting a Blazor Server web application on an Azure App Service. Users can upload files to an Azure Blob Storage account. I generate a token and build a URI for the upload. Following most every article I have read, I am copying the file from the user's PC to the web application in either a MemoryStream or FileStream, then uploading the file from the web application to the Blob Storage using Azure.Storage.Blobs.BlobClient.Upload().

This two-step process is slow and inefficient as the file is being uploaded twice, once to the Azure App Service, and again to the Blob Storage.

I would like for a user to be able to upload directly to the Blob Storage, using Azure.Storage.Blobs so that I can use a Progress Handler to update a progress bar.

I have watched videos, read articles, and everything seems to point to Microsoft articles that provide an example similar to what I have done. I have read articles about using javascript, but I could not get any solutions to work with Blazor. Below is a simplified version of my code.

    public static async Task UploadFileToAzureBlobStorage(IBrowserFile fileToUpload, Progress<long> progressHandler, Progress<long> copyProgressHandler)
    {
        // Create URL
        Uri blobUri = new Uri($"https://storageAccountName.blob.core.windows.net/containerName/filename.txt?token");

        // Create blob client from URL
        BlobClient blobClient = new BlobClient(blobUri);

        // Resolve full path to file
        string filename = Path.Combine("folderName", "filename.txt");

        // Upload file
        FileStream stream = new FileStream(filename, FileMode.Create);
        try
        {
            // Upload file to web server
            await fileToUpload.OpenReadStream(fileToUpload.Size).CopyToAsync(stream, copyProgressHandler);
            
            // Upload file from web server to Azure blob storage
            stream.Position = 0;
            await Task.Run(() => blobClient.Upload(stream, progressHandler: progressHandler));
        }
        catch (Exception ex)
        {
            // Error handling
        }

        // Dispose of stream
        await stream.DisposeAsync();
    }
like image 667
Eric Avatar asked Oct 11 '25 11:10

Eric


1 Answers

I was ultimately able to solve this issue by using the Azure Blob Storage client library for Node.js. While there were some challenges in communicating between the node.js application and the Blazor server, I found solutions for each challenge and now have a solution that uploads a file of any size from the user's browser to the Azure Storage, with a progress bar.

Here's what I did:

  1. Created a folder in the project's root folder named NodeJsLibraries.

  2. Right-click the new folder and open in Terminal.

  3. Type in a command to create a package.json file that will store the information and dependencies of the npm project:

    npm init
    
  4. Hit ENTER on all prompts, accepting the defaults.

  5. Installed the Azure Storage Blob package with this command:

    npm install @azure/storage-blob 
    
  6. Created a sub folder named src in the NodeJsLibraries folder.

  7. In the src sub folder, created a JavaScript file named index.js. Here is my JavaCcript:

    const { BlobServiceClient } = require('@azure/storage-blob');
    
    window.UploadFile = async function(accountName, sasToken, containerName, blobName, inputElement, fileNumber, fileUploadId) {
        // Reset cancelUpload flag
        window.cancelUpload = false;
        return new Promise(async (resolve, reject) => {
            try {
                // Resolve file element
                let file = inputElement.files[fileNumber];
                const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net?${sasToken}`);
                const containerClient = blobServiceClient.getContainerClient(containerName);
                const blockBlobClient = containerClient.getBlockBlobClient(blobName);
                const uploadBlobResponse = await blockBlobClient.uploadData(file, {
                    blockSize: 4 * 1024 * 1024, // 4MB blocks
                    onProgress: (progress) => {
                        // Check if a cancellation has been requested
                        if (window.cancelUpload) {
                            reject('Upload cancelled');
                            return;
                        }
    
                        // Report the upload progress to the UI
                        window.DotNet.invokeMethodAsync('<project_name>', '<project_name>.JavaScriptEvents.UpdateProgress', progress.loadedBytes, fileUploadId);
                    }
                });
                console.log(`Upload block blob ${blobName} successfully`, uploadBlobResponse.requestId);
                resolve(uploadBlobResponse);
            } catch (error) {
                reject(error);
            }
        });
    };
    
    window.requestUploadCancellation = function () {
        window.cancelUpload = true;
    };
    
  8. Installed webpack and webpack-cli as dev dependencies by running the following in the NodeJsLibraries folder. (This will allow you to use webpack to bundle your JavaScript code and dependencies):

    npm install webpack webpack-cli --save-dev
    
  9. Created a file in the NodeJsLibraries folder named webpack.config.js and configured webpack to output the bundled file into the wwwroot/js folder of my Blazor project. This is the file content:

    const path = require('path');
    
     module.exports = {
       entry: './src/index.js',
       output: {
         filename: 'azure_blob.js',
         path: path.resolve(__dirname, '../wwwroot/js'),
         library: 'AzureBlob'
       }
     };
    
  10. Edit the package.json file, adding the following inside the <script> tags:

    "build": "webpack --mode production"
    
  11. Added a reference to the azure_blob.js file in the _Host.cshtml file:

    <script src="js/azure_blob.js"></script>
    
  12. Bundled my JavaScript code and dependencies into a single file named azure_blob.js in the wwwroot/js folder using this command. (NOTE: Anytime you change the index.js file, you must re-run this command):

    npm run build
    
  13. Created a static class named JavaScriptEvents with methods UpdateProgress(), which is called by the JavaScript, and GetProgress(), which is called by my upload code to find out how many bytes have been loaded. I assign a unique FileId for each upload, which is parsed to the JavaScript, and used to track different file uploads in different sessions.

  14. In Blazor, we can access the IBrowserFile object for uploading a file. This object cannot be parsed to node.js as it's a .NET type that is not recognized. Instead, I created a reference to the InputFile element and parsed that reference along with the file # (if multiple files were selected) which node.js can handle.

    HTML:

    <InputFile id="file" @ref="uploadFile" OnChange="UploadFileAsync" />
    

    Declaration in my class:

    /// <summary>
    /// Reference to inputFile element; used by node.js to resolve file for upload (IBrowserFile object cannot be parsed via javascript interop as it's not recognized by node.js)
    /// </summary>
    InputFile uploadFile;
    
  15. This code is used in my Upload() method. A CancellationToken is created and referenced when calling the JavaScript via the JavaScript runtime; this is done to prevent a timeout of 60 seconds from cancelling the task if the upload takes more than a minute.

            // Start stopwatch to time
            Stopwatch stopwatch = Stopwatch.StartNew();            
     var cancellationTokenSource = new CancellationTokenSource();
            Task uploadTask = this.jsRuntime.InvokeVoidAsync(
            "UploadFile",
            cancellationTokenSource.Token,
            this.azureSas.StorageAccount,
            this.azureSas.Token,
            this.azureSas.ContainerName,
            blobName,
            inputFileElement, // Reference to the InputFile element
                0, // 0 = the file # in the list of files selected
                fileUploadId).AsTask();
                while (true)
                {
                    // Wait for either the upload task to complete or a short delay
                    TimeSpan timeElapsed = stopwatch.Elapsed;
                    Task completedTask = await Task.WhenAny(uploadTask, this.UpdateProgressAsync(fileUploadId, filesize, timeElapsed.TotalSeconds)).ConfigureAwait(false);
    
                    // If the upload task completed, exit the loop
                    if (completedTask == uploadTask)
                    {
                        Debug.Print($"Upload is complete: {uploadTask.IsCompleted} {uploadTask.IsCompletedSuccessfully} {uploadTask.Status}");
                        break;
                    }
                }
    
                // Handle the result or exception of the task
                if (uploadTask.IsFaulted)
                {
                    this.errorText = uploadTask.Exception?.InnerException?.Message ?? "Unknown error";
                    if (this.errorText == "Upload cancelled")
                    {
                        this.progress = string.Empty;
                        this.uploadStatus = "Upload cancelled by user";
                    }
    
                    return;
                }
            }
            catch (Exception exception)
            {
                this.errorText = exception.Message;
                return;
            }
    
            this.uploadStatus = "Upload complete";
    
  16. Here's the method for updating progress:

    private async Task UpdateProgressAsync(string fileUploadId, long fileSize, double secondsElapsed)
    
    {
            long bytesUploaded = JavaScriptEvents.GetProgress(fileUploadId);
    
        // When bytes uploaded are 1 or more, change the upload status to "Uploading..."
        if (bytesUploaded > 0 && this.uploadStatus != "Uploading...")
        {
            this.uploadStatus = "Uploading...";
        }
    
        // Create string with MB/s upload speed
        string speed = CommonMethods.SizeSuffix((long)(bytesUploaded / secondsElapsed)) + "/s";
    
        // Create string to display percentage uploaded
        this.progress = (long)((bytesUploaded / (double)fileSize) * 100) + "%        " + speed  + "        " + CommonMethods.SizeSuffix(bytesUploaded) + "/" + CommonMethods.SizeSuffix(fileSize);
    
        // Refresh the UI to display progress
        await InvokeAsync(this.StateHasChanged);
    
        await Task.Delay(500).ConfigureAwait(false);
    }
    
  17. I also added a Cancel button that calls this method:

    private async Task CancelUpload()
    {
        await jsRuntime.InvokeVoidAsync("requestUploadCancellation");
    }
    

I hope this helps someone else that wants to upload large files to Azure Storage from a Blazor Server application without following Microsoft's recommendations, which results in the file being copied to the server first, then being uploaded to Azure Storage.

like image 158
Eric Avatar answered Oct 14 '25 00:10

Eric