3/24/2024 Admin

Blazor WebAssembly Virtual File System Access


image

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

image

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.

 

image

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));
            }
        }
    }

 

image

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

image

 

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.";
        }
    }

 

image

 

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

image 

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:

 

  1. Zip up the current Directories and Files
  2. Store the Zip content in LocalStorage

 

Perform the following steps when the user returns to the page:

 

  1. Retrieve the Zip content from LocalStorage
  2. 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

image

 

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):

 

image

 

Managing The Zip File

image

 

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>&nbsp;&nbsp;
        <button class="btn btn-secondary" @onclick="DownloadZipFile">Download Zip File</button>&nbsp;&nbsp;
        <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);
    }

 

image

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.

 

Links

Blazored LocalStorage

Detecting navigation events

Handle/prevent location changes

An error has occurred. This application may no longer respond until reloaded. Reload 🗙