3/24/2024 Admin
Blazor WebAssembly Virtual File System Access
Blazor WebAssembly allows you to access the virtual file system in the web browser. This allows you to use the standard C# file system code to create directories and read and write files.
The only disadvantage is that the directories and files disappear when the web browser is refreshed. In this article we will demonstrate how to zip up any directories and files and store them in LocalStorage that will persist between web browser sessions. We will also add code that will detect when the Blazor app is being closed and automatically create the Zip file.
You can download the code from the GitHub repository at: https://github.com/ADefWebserver/BlazorWebAssemblyTempFile
Live Example
https://adefwebserver.github.io/BlazorWebAssemblyTempFile/
Creating Directories and Files
First, we will create the markup for a Status label, and a button that will count the existing Directories and Files:
<p><b>Status:</b> @status</p><button class="btn btn-info" @onclick="CountFiles">Count Directories And Files</button>
The button calls the following method:
private async Task CountFiles()
{directoryCount = 0;fileCount = 0;try
{status = "Counting directories and files...";
// Call method to count directories and files
await CountItems(BasePath);status = $"Directories = {directoryCount} and files = {fileCount}.";
}catch (Exception ex)
{status = ex.Message;return;
}}
This calls the following CountItems method:
private async Task CountItems(string path){// If BasePath is not a directory, return
if (!Directory.Exists(BasePath))
{status = "BasePath is not a directory.";
return;
}// Count the current directory
directoryCount++;// Get all files in the current directory and increment the file count
fileCount += Directory.GetFiles(path).Length;// Get all subdirectories in the current directory
string[] subDirectories = Directory.GetDirectories(path);
// Recursively count the items in all subdirectories
foreach (string dir in subDirectories){await Task.Run(() => CountItems(dir));}status = $"Directories = {directoryCount} and files = {fileCount}.";
StateHasChanged();}
Notice this code uses the normal C# file access code.
When we run the code and click the button, we see that we don’t have any Directories or Files.
Next, we add another button to create Directories and Files that calls the following code:
private async Task CreateFiles()
{status = "Creating directories and files...";
// Call method to create directories and files
await CreateDirectoriesAndFiles(BasePath, 2, 10);status = "Directories and files created.";
FilesExist = true;
}private async Task CreateDirectoriesAndFiles(string basePath, int dirCount, int filesPerDir){// If the base path does not exist, create it
if (!Directory.Exists(basePath))
{Directory.CreateDirectory(basePath);}// Create a string of 10,000 characters
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 10000; i++){sb.Append("data" + i + " ");}string data = sb.ToString();
// Create directories and files
for (int dirNum = 1; dirNum <= dirCount; dirNum++){string dirPath = Path.Combine(basePath, "Directory" + dirNum.ToString());Directory.CreateDirectory(dirPath);for (int fileNum = 1; fileNum <= filesPerDir; fileNum++){string filePath = Path.Combine(dirPath, "File" + fileNum.ToString() + ".txt");await Task.Run(() => File.WriteAllText(filePath, data));}}}
Now when we run the application, we can click the Create Directories and Files button to create the sample Directories and Files,
then click the Count Directories And Files button to display the count in the Status label.
(Note: The code only creates 2 directories, but the directory count is 3 because it is also counting the root directory)
Viewing and Editing a File
Next, we will add a button that will open a file and display it for editing.
We will use the following code to load the file:
private void LoadFile(){// Load the file into the textarea
string filePath = BasePath + @"/Directory1/File5.txt";if (File.Exists(filePath))
{// Read the file into a string
fileContent = File.ReadAllText(filePath);StateHasChanged();}else
{fileContent = "File not found.";
}}
At this point the user can make changes to the data stored in the file by editing the contents and clicking the Update File button.
We use the following code for the update:
private void UpdateFiles(){string filePath = BasePath + @"/Directory1/File5.txt";if (File.Exists(filePath))
{// Write the file content to the file
File.WriteAllText(filePath, fileContent);status = "File updated.";
}else
{fileContent = "File not found.";
}fileContent = "";}
The key thing to note is that loading the file and saving updates is very fast.
This is why you would want to use this method. Manipulating files and their content is extremely fast and easy.
Persisting The Directories and Files
If you refresh your web browser and click the Count Directories And Files button, you will see that all the Directories and Files are gone.
To persist files between web browser refreshes we can do the following before a web browser refresh:
- Zip up the current Directories and Files
- Store the Zip content in LocalStorage
Perform the following steps when the user returns to the page:
- Retrieve the Zip content from LocalStorage
- Unzip the Zip content into the virtual file system
We will use Blazored LocalStorage to access the web browser’s LocalStorage.
Zipping Up The Files
To Zip up the files we use the following code:
private async Task ZipTheFiles()
{string zipPath = @"/Zip";string zipFilePath = @"/Zip/ZipFiles.zip";// If zipFilePath exists, delete it
if (File.Exists(zipFilePath))
{File.Delete(zipFilePath);}// Create the directory if it doesn't exist
if (!Directory.Exists(zipPath))
{Directory.CreateDirectory(zipPath);}// If BasePath is not a directory, return
if (!Directory.Exists(BasePath))
{status = "Nothing to Zip up.";
return;
}// Create a zip file from the directory
ZipFile.CreateFromDirectory(BasePath, zipFilePath);status = "Files zipped.";
StateHasChanged();// Read the Zip file into a byte array
status = "Read the Zip file into a byte array";
byte[] exportFileBytes = File.ReadAllBytes(zipFilePath);
StateHasChanged();// Convert byte array to Base64 string
status = "Convert byte array to Base64 string";
string base64String = Convert.ToBase64String(exportFileBytes);StateHasChanged();// Store base64String in the browser's local storage
await localStorage.SetItemAsync("ZipFiles.zip", base64String);ZipFileExists = true;
status = "Zip file stored in the browser's local storage";
}
When we run the application, create the sample Directories and Files, then click the Zip Up Files button, we can see the following in the web browser’s LocalStorage (hit the F12 key to access the DevTools of the web browser):
Managing The Zip File
We want to detect that a Zip file exists in LocalStorage and to unzip it into the file system. We also want the ability to download and delete the Zip file.
We add the following markup code to display the buttons:
@if (ZipFileExists){<p><button class="btn btn-secondary" @onclick="UnzipFile">Unzip File To Directory</button> <button class="btn btn-secondary" @onclick="DownloadZipFile">Download Zip File</button> <button class="btn btn-danger" @onclick="DeleteZipFile">Delete Zip File</button></p><hr />}
We add the following code that will show the buttons on page load if the Zip file exists in LocalStorage:
// Run on page load
protected override async Task OnInitializedAsync(){// Check if the Zip file exists in local storage
ZipFileExists = await localStorage.ContainKeyAsync("ZipFiles.zip");
}
To unzip the file, we use the following code:
private async Task UnzipFile()
{string extractPath = @"/Zip";// If the extract directory does not exist, create it
if (!Directory.Exists(extractPath))
{Directory.CreateDirectory(extractPath);}// Get exportFileString from the browser's local storage
string exportFileString = await localStorage.GetItemAsync<string>("ZipFiles.zip");// Convert the Base64 string to a byte array
byte[] exportFileBytes = Convert.FromBase64String(exportFileString);
// Write the byte array to a file
await File.WriteAllBytesAsync($"{extractPath}/ZipFiles.zip", exportFileBytes);
// Extract the zip file
ZipFile.ExtractToDirectory($"{extractPath}/ZipFiles.zip", BasePath);
status = "Files unzipped.";
FilesExist = true;
}
To download the Zip file, we first add the following JavaScript method to the Index.html page:
<script>window.saveAsFile = async (filename, base64String) => {// Convert Base64 string to a Blob
const byteCharacters = atob(base64String);const byteNumbers = new Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {byteNumbers[i] = byteCharacters.charCodeAt(i);}const byteArray = new Uint8Array(byteNumbers);const blob = new Blob([byteArray], { type: "application/zip" });// Create a link element and download the file
const link = document.createElement('a');link.href = window.URL.createObjectURL(blob);
link.download = filename;link.click();}</script>
Then in the Home.razor page we add:
@inject IJSRuntime JsRuntime
Then the following method:
private async Task DownloadZipFile()
{// Get exportFileString from the browser's local storage
string base64String = await localStorage.GetItemAsync<string>("ZipFiles.zip");// Download the zip file
await JsRuntime.InvokeVoidAsync("saveAsFile", "ZipFiles.zip", base64String);}
We can now download and view the zipped up Directories and Files.
To delete the Zip file, we use the following code:
private void DeleteZipFile(){// Remove the zip file from the browser's local storage
localStorage.RemoveItemAsync("ZipFiles.zip");
ZipFileExists = false;
}
How To Save Files Automatically When the Web Browser Closes
The issue we now have is that the Directories and Files will only be zipped up if the user clicks the button to Zip them up.
What happens if the user closes the web browser without doing this?
We can add code that will detect when a Blazor app is being closed and automatically create the Zip file.
First, we add the following JavaScript method to the Index.html page:
<script>window.setupBeforeUnload = function (callback) {window.addEventListener("beforeunload", function (event) {callback.invokeMethodAsync("HandleBeforeUnload");
});};</script>
In the Home.razor page we add the following field (and code to dispose of it properly):
private DotNetObjectReference<Home> objRef;
public void Dispose(){objRef?.Dispose();}
We add the following code to OnInitializedAsync():
// Run on page load
protected override async Task OnInitializedAsync(){objRef = DotNetObjectReference.Create(this);
await JsRuntime.InvokeVoidAsync("setupBeforeUnload", objRef);
}
This will call the JavaScript created in the previous step.
That JavaScript will call a HandleBeforeUnload method when it detects that the web browser is being closed.
Next, we add the following code to implement that method:
[JSInvokable]public void HandleBeforeUnload(){status = "User is navigating away from the page - Zip any files";
// Zip up any files and store in LocalStorage
Task.Run(async () => await ZipTheFiles());}
This will Zip up the files.
Detect When User Navigates Away
We also want to automatically Zip up the files when the user navigates away from the page.
First, we add the following:
@inject NavigationManager Navigation
Then we add the following two methods:
protected override void OnAfterRender(bool firstRender){if (firstRender)
{registration =Navigation.RegisterLocationChangingHandler(OnLocationChanging);}}private ValueTask OnLocationChanging(LocationChangingContext context)
{// Get the base URL
string baseUrl = Navigation.BaseUri;
// Detect that user is going to counter page
if (context.TargetLocation != baseUrl)
{status = "User is navigating away from the page - Zip any files";
// Zip up any files and store in LocalStorage
Task.Run(async () => await ZipTheFiles());}return ValueTask.CompletedTask;
}
Download
The project is available at: https://github.com/ADefWebserver/BlazorWebAssemblyTempFile
You must have Visual Studio 2022 (or higher) installed to run the code.