PROWAREtech

articles » current » blazor » wasm » compress-and-upload-files

Blazor: Compress and Upload Files

Example of compressing and uploading files to a Web API or REST API controller in Blazor WebAssembly (WASM).

This example uses .NET 6. It is compatible with .NET 8 and .NET 5 but incompatible with earlier versions of .NET including .NET Core 3.1.

This is an example of using the InputFile of .NET 6. It also uses tasks which are similar to threads.

This example involves compressing files with GZip and then converting them into base64 strings to be uploaded via JSON to the server. It compresses on the client-side and decompresses on the server-side. NOTE: it only compresses files that have room to shrink and are not already compressed. If they are of a compressed format then they are simply stored.

This example uploads any kind of file. For an image specific version, see this article.

Create a new .NET 5 (or later) Blazor WASM project with an ASP.NET Core backend.

Add the Compression Class

The Compressor class will compress and decompress bytes using GZip. This is code from the GZipStream article. The Brotli algorithm would have been nice to use but it is not current supported by Blazor WASM.

// Compressor.cs
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;

public class Compressor
{
	public static async Task<byte[]> CompressBytesAsync(byte[] bytes, CancellationToken cancel = default(CancellationToken))
	{
		using (var outputStream = new MemoryStream())
		{
			using (var compressStream = new GZipStream(outputStream, CompressionLevel.Optimal))
			{
				await compressStream.WriteAsync(bytes, 0, bytes.Length, cancel);
			}
			return outputStream.ToArray();
		}
	}

	public static async Task<byte[]> DecompressBytesAsync(byte[] bytes, CancellationToken cancel = default(CancellationToken))
	{
		using (var inputStream = new MemoryStream(bytes))
		{
			using (var outputStream = new MemoryStream())
			{
				using (var decompressStream = new GZipStream(inputStream, CompressionMode.Decompress))
				{
					await decompressStream.CopyToAsync(outputStream, cancel);
				}
				return outputStream.ToArray();
			}
		}
	}
}

Add New File Class (FileX) to Project

In the root of the shared project, create a file named FileX.cs.

// FileX.cs
using System.Text.Json.Serialization;

public class FileX
{
	public string base64string { get; set; }
	public string contentType { get; set; }
	public string fileName { get; set; }
	public byte state { get; set; }

	[JsonIgnore]
	public byte[] buffer { get; set; }
	[JsonIgnore]
	public int originalSize { get; set; }
	[JsonIgnore]
	public int compressedSize { get; set; }

	public bool IsOkToCompress()
	{
		if (contentType != null)
		{
			var low = contentType.ToLower();
			if (low.StartsWith("image/"))
				return low.EndsWith("/svg+xml") || low.EndsWith("/bmp");
			if (low.StartsWith("audio/") || low.StartsWith("video/"))
				return low.EndsWith("/wav");
			if (low.StartsWith("text/"))
				return true;
			if(low.StartsWith("application/"))
			{
				switch(low.Split('/')[1])
				{
					case "x-abiword":
					case "octet-stream": // assume it can be compressed
					case "x-csh":
					case "x-msword":
					case "vnd.openxmlformats-officedocument.wordprocessingml.document":
					case "json":
					case "ld+json":
					case "vnd.apple.installer+xml":
					case "vnd.oasis.opendocument.presentation":
					case "vnd.oasis.opendocument.spreadsheet":
					case "vnd.oasis.opendocument.text":
					case "x-httpd-php":
					case "vnd.ms-powerpoint":
					case "vnd.openxmlformats-officedocument.presentationml.presentation":
					case "rtf":
					case "x-sh":
					case "vnd.visio":
					case "xhtml+xml":
					case "vnd.ms-excel":
					case "vnd.openxmlformats-officedocument.spreadsheetml.sheet":
					case "xml":
					case "vnd.mozilla.xul+xml":
						return true;
				}
			}
		}
		return false;
	}
}

Create the Upload Controller

In the root of the server project, create a file named UploadController.cs.

// UploadController.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System;
using Microsoft.AspNetCore.Hosting;
using System.Threading;
using System.IO;

namespace UploadFiles.Server.Controllers
{
	[ApiController]
	[Route("api/[controller]")]
	public class UploadController : ControllerBase
	{
		private readonly IWebHostEnvironment env;

		public UploadController(IWebHostEnvironment env)
		{
			this.env = env;
		}

		[HttpPost]
		public async Task Post([FromBody] FileX[] files, CancellationToken cancel)
		{
			foreach (var file in files)
			{
				var buf = Convert.FromBase64String(file.base64string);
				if((file.state & 2) != 0)
					buf = await Compressor.DecompressBytesAsync(buf, cancel);
				await System.IO.File.WriteAllBytesAsync(Path.Combine(env.ContentRootPath, Guid.NewGuid().ToString("N") + "-" + file.fileName), buf, cancel);
			}
		}
	}
}

Modify Index.razor

In the client project, modify Index.razor or simply create a new page with any path.

@page "/"
@inject HttpClient Http

<h1>Compress and Upload Files</h1>

<div class="input-group">
	<div class="custom-file">
		<InputFile class="custom-file-input" multiple OnChange="OnChange" id="inputFile" />
		<label class="custom-file-label" for="inputFile">Choose and compress file(s) for upload</label>
	</div>
	<div class="input-group-append">
		<button class="btn btn-success" @onclick="Upload" disabled="@isDisabled">Upload</button>
	</div>
</div>

<div class="container-fluid my-3">
	<h5>@message</h5>
</div>

<ul class="list-group">
@foreach (var file in files)
{
    <li class="list-group-item">@file.fileName
	@if ((file.state & 4) != 0 || (file.state & 2) != 0)
	{
		if ((file.state & 4) != 0)
		{
		<span class="text-success mx-1">(&#9989; stored @System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(file.base64string)).Length bytes)</span>
		}
		if ((file.state & 2) != 0)
		{
		<span class="text-success mx-1">(&#9989; compressed original=@file.originalSize, compressed=@file.compressedSize)</span>
		}
	}
	else if ((file.state & 1) != 0)
	{
        <div class="spinner-grow spinner-grow-sm text-warning mx-1" role="status">
            <span class="sr-only">processing...</span>
        </div>
	}
    else
	{
		<span class="text-primary mx-1">&#8987; waiting...</span>
	}
    </li>
}
</ul>

@code {
	List<FileX> files = new List<FileX>();
	bool isDisabled = true, selectFilesOk = true;
	string message;

	async Task OnChange(InputFileChangeEventArgs e)
	{
		message = string.Empty;

		if (!selectFilesOk)
			return;

		selectFilesOk = false;
		IReadOnlyList<IBrowserFile> bfiles = default(IReadOnlyList<IBrowserFile>);
		int index = -1;

		try
		{

			bfiles = e.GetMultipleFiles(); // get the files selected by the user
			for (index = 0; index < bfiles.Count; index++)
			{
				var f = new FileX { buffer = new byte[bfiles[index].Size], contentType = bfiles[index].ContentType, fileName = bfiles[index].Name };
				using (var stream = bfiles[index].OpenReadStream())
				{
					await stream.ReadAsync(f.buffer);
				}
				files.Add(f);
			}

			index = -1;
			StateHasChanged();
			var tasks = new List<Task>(); // create a list of tasks

			foreach (var file in files)
			{
				if (file.state == 0)
				{
					Task task = Task.Run(async () =>
					{
						file.state |= 1; // let the app know this file has begun processing
						file.originalSize = file.buffer.Length;
						StateHasChanged();

						if (file.IsOkToCompress()) // only compress files that will benefit from it
						{
							file.buffer = await Compressor.CompressBytesAsync(file.buffer); // compress the file buffer
							file.compressedSize = file.buffer.Length;
							file.state |= 2;
							StateHasChanged();
						}

						file.base64string = Convert.ToBase64String(file.buffer); // convert the compressed data to a base64 string
						file.state |= 4; // let the app know this file has been stored
						StateHasChanged();
					});
					tasks.Add(task); // add task to the list
				}
			}

			await Task.WhenAll(tasks); // wait for the tasks to finish

		}
		catch (Exception ex)
		{

			message = ex.Message;
			if (index > -1) // could've just used another try block inside the above try block!
				message += " (" + bfiles[index].Name + ")";

		}

		isDisabled = false; // enable the upload button
		selectFilesOk = true; // allow adding more files
	}

	async Task Upload()
	{
		isDisabled = true;
		using (var msg = await Http.PostAsJsonAsync<List<FileX>>("/api/upload", files, System.Threading.CancellationToken.None))
		{
			isDisabled = false;
			if (msg.IsSuccessStatusCode)
			{
				message = $"{files.Count} files uploaded";
				files.Clear();
			}
		}
	}
}

Coding Video

https://youtu.be/f6uulAcHQmE


This site uses cookies. Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site. Read the privacy policy.
CLOSE