Skip to content

CheerpJ filesystem tutorial #264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 302 additions & 0 deletions sites/cheerpj/src/content/docs/11-guides/filesystem.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
---
title: Filesystem
description: Interacting with the virtual filesystem in CheerpJ
---

CheerpJ provides a **virtual filesystem** that allows your Java applications to perform file operations like reading and writing, just as they would on a desktop. This guide will explain how to effectively use CheerpJ's virtual filesystem, demonstrating how to transfer files between your Java application and the local JavaScript environment by focusing on common use cases.

## CheerpJ's Virtual Filesystem: Mounting Points Explained

CheerpJ's virtual filesystem has different **mounting points**, each with specific purposes:

- **`/app/`**: This mount point is **read-only**. It corresponds to the root of the web server and contains all the files that are part of your Java application, including your `.jar` files and any resources bundled within them. Java can read files from `/app/`, but it **cannot write** to this location.
- **`/files/`**: This is a **persistent, in-memory filesystem**. Files written here are accessible by your Java application and persist across sessions and page reloads until the user manually clears their browser's IndexedDB. This is useful for user-generated data, or situations where you need to move files around within the virtual filesystem.
- **`/str/`**: This mounting point is for adding string content directly from JavaScript into the virtual filesystem. This is useful for providing configuration files or initial data to your Java application without needing to package them inside your JAR.

Read more about the filesystem in the [CheerpJ documentation](/docs/explanation/File-System-support).

> [\!info] Local files
> CheerpJ provides access to a virtualized filesystem, which does not correspond to the local user's computer. For browser security reasons, direct access to local files is forbidden. This means your Java code running in CheerpJ cannot directly read or write files on the user's physical hard drive. All file interactions happen within this secure, virtual environment.

## 1\. Loading Files from JavaScript into the Virtual Filesystem

Often, you might have files on your web page (e.g., uploaded by a user) that your Java application needs to process. You can transfer these files from the JavaScript environment into CheerpJ's virtual filesystem. This is useful for:

- **External Applications:** Sometimes, your Java application expects files to be in a specific location (e.g., a hardcoded path like "filename.txt"). If you load a file into `/app/`, your Java code would need to reference it as `/app/filename.txt`. However, if your Java application is an external application that you cannot modify, it might not be able to change this prefix. Loading the file into `/files/` allows Java to access it using just "filename.txt", as `/files/` behaves more like a default working directory.
- **Dynamic Content (e.g., Configuration Files):** You can provide dynamic content, such as configuration files, to your Java application directly from JavaScript, without recompiling your Java code.
- **Binary Data:** The `cheerpOSAddStringFile` API (and the underlying `lib.java.nio.file.Files.copy` method) can handle binary data, making it useful for providing various file types to Java.

### Method 1: Using `cheerpOSAddStringFile` API (for string or binary data)

This is the simplest way to add a file from JavaScript to the virtual filesystem. It's particularly useful for providing string content, but can also handle binary data.

**Important:** `cheerpOSAddStringFile` must be called after `cheerpjInit()` has completed (i.e., its Promise has resolved), as it relies on the CheerpJ runtime environment and its virtual filesystem being initialized.

The `cheerpOSAddStringFile` API allows you to create a file in the `/str/` mount point, which can then be accessed by your Java application as if it were a regular file.

Example:

```js title="index.html"
// Function to add a string file to /str/ from JavaScript
async function addStringFileToCheerpJ() {
const fileName =
document.getElementById("jsFileName").value.trim() || "default.txt";
const content =
document.getElementById("jsFileContent").value || "Default content.";
const messageDiv = document.getElementById("jsMessage");

try {
// Using cheerpOSAddStringFile to add content to /str/
await cheerpOSAddStringFile("/str/" + fileName, content);
messageDiv.textContent = `File "${fileName}" added successfully to /str/.`;
console.log(`File "${fileName}" added to /str/.`);
} catch (e) {
messageDiv.textContent = `Error adding file to /str/: ${e.message}`;
console.error("Error writing file to /str/:", e);
}
}
```

You can find the complete code for this example in the [template project](https://github.com/leaningtech/cheerpj-meta/examples/Filesystem/) under the `com/filereader` directory. To run this example locally, follow the steps from the [Running the Examples](#running-the-examples) section.

### Method 2: Using `lib.java.nio.file.Files.copy` (for files already in `/app/` or complex scenarios)

If you have a file that's already part of your `/app/` mount point, but your Java application needs to access it as if it were in `/files/` (without the `/app/` prefix), you can copy it using Java's `nio.file` APIs via library mode in JavaScript. This allows Java applications that expect files in a "local" directory to find them.

```javascript title="index.html"
async function copyFileToFilesMountPoint() {
// cheerpjRunLibrary can be called before cheerpjInit
const lib = await cheerpjRunLibrary(""); // Create a library mode object
const Files = await lib.java.nio.file.Files;
const StandardCopyOption = await lib.java.nio.file.StandardCopyOption;
const Paths = await lib.java.nio.file.Paths;

// Source is a file within the /app/ mount point
const source = await Paths.get(`/app/notes_tmp.txt`, []);
// Target is the /files mount point
const target = await Paths.get(`/files/notes_tmp.txt`, []);

try {
await Files.copy(source, target, [StandardCopyOption.REPLACE_EXISTING]);
console.log("File copied from /app/ to /files/ successfully!");
} catch (e) {
console.error("Error copying file:", e);
}
}

// Then, initialize CheerpJ and run your Java application
async function cj3init() {
await cheerpjInit({
version: 8,
});
// cheerpjCreateDisplay(...) and cheerpjRunMain(...) go here
}
copyFileToFilesMountPoint();
cj3init();
// Your Java app can now access "notes_tmp.txt" by simply calling `new File("notes_tmp.txt")`
// without needing the /app/ prefix.
```

**Explanation:**

- **`cheerpjRunLibrary("")`**: This creates a library mode object that allows you to use Java's `nio.file` APIs in JavaScript. You can read more about library mode in the [CheerpJ documentation](/docs/reference/cheerpjRunLibrary).
- **`Files.copy(source, target, [StandardCopyOption.REPLACE_EXISTING])`**: This copies the file from the `/app/` mount point to the `/files/` mount point. The `REPLACE_EXISTING` option ensures that if the target file already exists, it will be overwritten.
- **Accessing the File in Java**: After copying, your Java application can access the file simply by using `new File("notes_tmp.txt")`, without needing to specify the `/app/` prefix.

To use this method, the file you want to copy must already exist in the `/app/` mount point. For example, if you have a file named `notes_tmp.txt` that has been made available in `/app/` (e.g., by being part of your CheerpJ application's resources), you can then copy it to `/files/`.

## 2\. Getting Files from the Virtual Filesystem to the Local Filesystem (Java to JavaScript)

This is a very common requirement: your Java application generates or modifies a file, and you need to access the file locally. It's important to note that when your Java application saves a file while running with CheerpJ, this file will be written to CheerpJ's virtual filesystem (the /files/ mount point) rather than directly to the user's local file system. The general workflow for then making this file available to the user is as follows:

1. **Java writes/generates a file:** Your Java application creates or saves a file to the `/files/` mounting point within CheerpJ's virtual filesystem.
2. **Java calls a JavaScript native method:** Since the file is in the virtual filesystem, we can implement a native to retrieve it in JavaScript.
To learn more about native methods, you can refer to the [CheerpJ tutorial on native methods](/docs/guides/implementing-native-methods.html).
3. **JavaScript retrieves and processes the file:** The JavaScript native method retrieves the file's content from the virtual filesystem. At this point, the JavaScript native code can process the file in any way required by your web application. For example you can trigger a download by using the `cjFileBlob()` API to get the file's content as a `Blob` and then use standard browser APIs to trigger a download (as will be explained in a later section).

**Step 1: Java Writes the File and Calls a Native Method**

First, your Java application needs to write the file to CheerpJ's virtual filesystem, specifically to the `/files/` mount point. Once the file is written, Java can call a _native method_ which acts as a bridge to execute JavaScript code.

```java title="App.java" {7}
try {
File file = new File(filePath);
FileWriter writer = new FileWriter(file);
writer.write(content);
writer.close();

downloadFileFromCheerpJ(filePath);

} catch (IOException e) {
System.err.println("Error generating or writing file: " + e.getMessage());
}
```

**Explanation:**

- The `downloadFileFromCheerpJ` method is declared as a native method. This means we will implement its functionality in JavaScript, allowing us to trigger file downloads from our Java application.
- `new File(fileName)`: This specifies that the file should be written with the given name in the `/files/` mount point.
- `downloadFileFromCheerpJ(...)`: After the file is written, this line calls our native method, passing the full path of the file within the virtual filesystem. This `filePath` is what JavaScript will use to retrieve the file.

**Step 2: JavaScript Implements the Native Method and Downloads the File**

Now, we need to provide the JavaScript implementation for the `downloadFileFromCheerpJ` native method. This JavaScript function will receive the file path from Java, use CheerpJ's `cjFileBlob()` API to get the file's content, and then use standard browser APIs to trigger a download.

```javascript title="index.html"
// JavaScript implementation of the native method
// The naming convention is Java_<fully-qualified-class-name>_<method-name>
async function Java_com_download_DownloadExample_downloadFileFromCheerpJ(
lib, // CheerpJ's internal library object (usually not needed here)
fileName // The file name passed from Java
) {
console.log(`Attempting to download file: ${fileName}`);
try {
// 3. Use cjFileBlob() to get the file content as a Blob
const blob = await cjFileBlob(`${fileName}`);
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
// Set the downloaded file name as the last part of the path
link.download = fileName.split("/").pop(); // Extract the file name from the path
document.body.appendChild(link); // Append to body to make it clickable
link.click(); // Programmatically click the link to trigger download
document.body.removeChild(link); // Clean up
URL.revokeObjectURL(link.href); // Release the object URL
console.log(`Successfully downloaded ${fileName}`);
} catch (e) {
console.error("Error downloading file:", e);
alert("Failed to download file. Check console for details.");
}
}

// Initialize CheerpJ and run the Java application
async function cj3init() {
await cheerpjInit({
version: 8,
// Register the native method
natives: {
Java_com_download_DownloadExample_downloadFileFromCheerpJ,
},
});
// cheerpjCreateDisplay(...) and cheerpjRunMain(...) go here
}
cj3init();
```

**Explanation:**

- **Native Method Naming Convention**: The JavaScript function name must follow the pattern `Java_<fully-qualified-class-name>_<method-name>`. This is how CheerpJ links your Java native method call to the correct JavaScript function.
- `cjFileBlob(filePath)`: This CheerpJ API is the bridge between the virtual filesystem and JavaScript. It takes the path to a file within `/files/` (or `/app/`, `/str/`) and returns its content as a `Blob`.
- **Blob and Download**: Once you have the `Blob`, standard browser techniques are used to create a temporary download link and simulate a click, prompting the user to save the file. `URL.createObjectURL(blob)` generates a temporary URL for the blob, and `link.download` suggests a filename to the browser.
- **`natives` Object in `cheerpjInit`**: This is where you register all your custom JavaScript native methods with CheerpJ, making them accessible from your Java code.

You can find the complete code for this example in the [template project](https://github.com/leaningtech/cheerpj-meta/examples/Filesystem/) under the `com/download` directory. To run this example locally, follow the steps from the [Running the Examples](#running-the-examples) section.

## 3\. Using Java's `JFileChooser` with CheerpJ

Java applications often use `JFileChooser` for "Save As..." dialogues. CheerpJ supports `JFileChooser`, and when a user selects a file name, that name is used within the virtual filesystem. You can then use this name to trigger a download using the native method approach described in Section 2.

When you instantiate `JFileChooser` in your Java code, it operates on CheerpJ's virtual filesystem. You instantiate it without arguments (`JFileChooser fileChooser = new JFileChooser();`), to open the dialog in the default directory: `/files/`.

**Java Source Code: Essential `JFileChooser` and Download Logic**

```java title="App.java"

// (Retain the native method declaration from Section 2)
public static native void downloadFileFromCheerpJ(String filePath);

// Button to trigger the JFileChooser
JButton chooseDownloadButton = new JButton("Choose File to Download");

chooseDownloadButton.addActionListener((ActionEvent e) -> {
// 1. Instantiate JFileChooser, initially pointing to the /files/ mount point.
JFileChooser fileChooser = new JFileChooser("/files/");
fileChooser.setDialogTitle("Select file to download");
// 2. Show the save dialog to let the user select a file.
int result = fileChooser.showSaveDialog(this);
// 3. Check if the user approved the selection (didn't cancel).
if (result == JFileChooser.APPROVE_OPTION) {
// 4. Get the selected file from the file chooser.
// For example, if you wrote "Hello World" to /files/example.txt, "/files/example.txt" will be the selected file.
// 5. Get the path of the selected file.
File selectedFile = fileChooser.getSelectedFile();
String filePath = selectedFile.getPath();
// 6. Call the native JavaScript method to download the file from the virtual filesystem.
downloadFileFromCheerpJ(filePath);
}
});
```

**Explanation:**

- `new JFileChooser("/files/")`: This initializes the file chooser to start Browse from the `/files/` directory within CheerpJ's virtual filesystem. This is important because `/files/` is the writable and persistent mount point.
- `fileChooser.showSaveDialog(this)`: This method displays the file chooser dialog, allowing the user to select a save location and filename. `showOpenDialog` would be used for opening existing files.
- `fileChooser.getSelectedFile().getPath()`: If the user approves the selection, this returns the full path (e.g., `/files/myDocument.txt`) that the user specified.
- `downloadFileFromCheerpJ(filePath)`: We reuse the same native method from Section 2. Once the user has selected a path, you would ensure your Java application writes its data to that `filePath` within the virtual filesystem, and then you would call this native method to trigger the download to the user's local machine.

**HTML and JavaScript Code (`index.html`)**

The HTML and JavaScript for handling the download are essentially the **same as in Section 2** with a difference in name of the native method: `Java_com_filechooser_FileChooserDownloadExample_downloadFileFromCheerpJ`. This is because the Java class is now `com.filechooser.FileChooserDownloadExample`, and the native method needs to match that.

You can find the complete code for this example in the [template project](https://github.com/leaningtech/cheerpj-meta/examples/Filesystem/) under the `com/filechooserdownload` directory. To run this example locally, follow the steps from the [Running the Examples](#running-the-examples) section.

## 4\. Special Case: Applets and `showDocument()`

For **Applets**, `AppletContext.showDocument()` can be used as a workaround for downloads, although native methods are generally more flexible.

- **Direct URL Download**: If the file you want to download is already available at a public URL (e.g., hosted on your server), you can simply use `showDocument()` to point to that URL, and the browser will handle the download.

```java
// Java Applet Code
private void downloadDirectURL() {
try {
// Assumes 'example.txt' is in the same directory as the applet JAR
URL fileUrl = new URL(getCodeBase().getProtocol(),
getCodeBase().getHost(),
getCodeBase().getPort(),
"/example.txt");
getAppletContext().showDocument(fileUrl, "_blank"); // "_blank" will trigger download
System.out.println("Triggered direct URL download: " + fileUrl.toString());
} catch (Exception ex) {
ex.printStackTrace();
JOptionPane.showMessageDialog(null, "Error downloading from URL: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
```

- **Executing JavaScript for Dynamic Content**: You can use `showDocument()` to execute arbitrary JavaScript code that then handles a download. This is less common but can be useful for passing dynamically generated content from Java to JavaScript for download.

```java
// Java Applet Code
private void downloadViaJavaScriptShowDocument() {
try {
String content = "Dynamic content generated in Java!";
String encodedContent = URLEncoder.encode(content, "UTF-8").replace("+", "%20"); // Encode for URL
AppletContext context = getAppletContext();
// Call a JavaScript function 'downloadFromJava' and pass content
context.showDocument(new URL("javascript:downloadFromJava('" + encodedContent + "')"));
System.out.println("Triggered JS download via showDocument.");
} catch (Exception ex) {
ex.printStackTrace();
}
}
```

And in your HTML, you'd have the `downloadFromJava` JavaScript function:

```javascript
// JavaScript in applet.html
function downloadFromJava(encodedContent) {
const content = decodeURIComponent(encodedContent);
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);

const a = document.createElement("a");
a.href = url;
a.download = "applet_showdocument_dynamic.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log("File downloaded via showDocument (JS)");
}
```