2023-08-06Simplifying File Saving with JavaScript Blobs and Wails: A Step-by-Step Guide

I was working on a project recently where I needed to create a file in the Javascript side of the application and save it to the filesystem. I was using Wails for the project because Golang is amazing! 😍 Wails supports a "SaveFileDialog" but at the time of writing this post, it does not support it on the frontend.

DialogNotSupported

Now, the solution below is not recommended for large files. This is because we will be passing the file by encoding the Blob as a base64 string. For large files, this will consume a lot of memory.

Step 1 - Create a new Wails project

To get started, we will create a new Wails project. I have named mine wails-save-blob. We will be using the React template.

wails init -n wails-save-blob -t react-ts

Once the project code, open it in your favorite editor. I am using VSCode. Be sure to run wails dev to make sure everything is working as expected.

Wails Dev

Step 2 - Add golang function to save blob

Open the app.go file and add the following function

func (a *App) SaveFile(title string, defaultFilename string, fileFilterDisplay string, fileFilterPattern string, base64Content string) string {
	file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
		Title:           title,
		DefaultFilename: defaultFilename,
		Filters: []runtime.FileFilter{{
			DisplayName: fileFilterDisplay,
			Pattern:     fileFilterPattern,
		}},
	})
	if err != nil {
		return err.Error()
	}

	// Decode base64 content
	bytes, err := base64.StdEncoding.DecodeString(base64Content)
	if err != nil {
		return err.Error()
	}

	// Write file
	err = os.WriteFile(file, bytes, 0644)
	if err != nil {
		return err.Error()
	}

	return file
}

This function will take in the following parameters:

Parameter Description
title The title of the dialog
defaultFilename The default filename
fileFilterDisplay The display name of the file filter
fileFilterPattern The file filter pattern
base64Content The base64 encoded content

The function will return the path of the file that was saved.

Step 3 - Add Javascript function to save blob

Let's do some cleanup in the template. First delete the assets folder and Delete App.css and style.css; Remove the style.css import from main.tsx. Finally, replace App.tsx with the following:

import { useState } from "react";
import { SaveFile } from "../wailsjs/go/main/App";

export default function App() {
  const [isSaving, setIsSaving] = useState(false);

  const createCsvString = () => {
    const csvHeaders = "City, State";
    const csvRows = [
      ["New York City", "New York"],
      ["Los Angeles", "California"],
      ["Chicago", "Illinois"],
    ];
    const csvString = [csvHeaders, ...csvRows.map((row) => row.join(","))].join("\n");
    return csvString;
  };

  const blobToBase64 = (blob: Blob) => {
    const reader = new FileReader();
    reader.readAsDataURL(blob);
    return new Promise<string>((resolve, reject) => {
      reader.onloadend = () => {
        const base64data = reader.result;
        if (typeof base64data === "string") {
          resolve(base64data.split(",")[1]);
        }

        reject("Base64 data is not a string");
      };
    });
  };

  const handleSave = async () => {
    setIsSaving(true);

    const csvString = createCsvString();
    const blob = new Blob([csvString], { type: "text/csv;charset=utf-8;" });

    const base64 = await blobToBase64(blob);

    const title = "Save cities.csv";
    const filename = "cities.csv";
    const fileFilderDisplay = "Csv File (*.csv)";
    const fileExtension = "*.csv";
    await SaveFile(title, filename, fileFilderDisplay, fileExtension, base64);
    setIsSaving(false);
  };

  return (
    <div id="App">
      <button className="btn" onClick={handleSave}>
        {isSaving ? "Saving... please wait" : "Save CSV File Blob"}
      </button>
    </div>
  );
}

This React component, will create a CSV string and convert it to a Blob. Then it will convert the Blob to a base64 string. Finally, it will call the SaveFile function that we created in the previous step.

Save CSV File Blob

Step 4 - Test it out

Now that we have everything setup, let's test it out. Run wails dev to start the application. Click on the button and you should see a dialog to save the file.

Save File Dialog

Downloads Folder

Let's open the file in Excel and make sure that everything is working as expected.

Excel

Limitations

  • This solution is not recommended for large files. This is because we will be passing the file by encoding the Blob as a base64 string. For large files, this will consume a lot of memory.
  • This solution currenlty only accepts a single file filter, but it can be easily modified to accept multiple file filters by storing the file filter parameters in a dictionary.

The source code used in this tutorial is available on GitHub.