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();
}
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:
Created a folder in the project's root folder named NodeJsLibraries
.
Right-click the new folder and open in Terminal.
Type in a command to create a package.json
file that will store the information and dependencies of the npm project:
npm init
Hit ENTER on all prompts, accepting the defaults.
Installed the Azure Storage Blob package with this command:
npm install @azure/storage-blob
Created a sub folder named src
in the NodeJsLibraries
folder.
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;
};
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
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'
}
};
Edit the package.json
file, adding the following inside the <script>
tags:
"build": "webpack --mode production"
Added a reference to the azure_blob.js
file in the _Host.cshtml
file:
<script src="js/azure_blob.js"></script>
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
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.
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;
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";
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);
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With