PROWAREtech
Blazor: Compress and Upload Files
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">(✅ 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">(✅ 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">⌛ 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();
}
}
}
}