# How to Build Custom Tauri Commands for Native Filesystem Access in GeoLibre

> Learn to build custom Tauri commands for native filesystem access. Securely use Rust and `std::fs` APIs with `app.fs_scope().is_allowed()` for your GeoLibre project.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-08-02

---

**To build custom Tauri commands for native filesystem access in GeoLibre, create async Rust functions annotated with `#[tauri::command]` that verify paths using `app.fs_scope().is_allowed(&path)` before calling standard `std::fs` APIs, then invoke them from the frontend using `invoke()`.**

GeoLibre's desktop application leverages Tauri v2 to bridge Rust's native performance with a modern web frontend. When you need to read, write, or manipulate files beyond the browser's sandbox, you must build custom Tauri commands for native filesystem access that respect the security boundaries defined in the app's configuration.

## Understanding Tauri Commands in GeoLibre

GeoLibre's desktop code lives under `apps/geolibre-desktop/src-tauri`. Native functionality is exposed to the JavaScript frontend through **Tauri commands**—async Rust functions annotated with `#[tauri::command]`.

According to the GeoLibre source code, any function carrying this attribute is automatically exposed to the frontend when the Tauri app is built. Commands typically accept serializable parameters and return `Result<T, String>` to handle both success and error cases gracefully.

The project already includes the official **`tauri-plugin-fs`** (see the `tauri_plugin_fs::FsExt` import in [`src/lib.rs`](https://github.com/opengeos/GeoLibre/blob/main/src/lib.rs)), which provides the security scaffolding for all filesystem operations.

## Security Model and Scope Verification

Before performing any filesystem operation, custom commands must verify that the user has explicitly granted access to the target path. The `fs` plugin restricts access to only paths within the allowed scope.

In [`apps/geolibre-desktop/src-tauri/src/lib.rs`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src-tauri/src/lib.rs) (lines 485‑487), the codebase demonstrates this pattern by calling `app.fs_scope().is_allowed(&path)` before any `std::fs` operation. This check ensures your command respects the sandbox imposed by the Tauri security model.

**Never bypass this verification.** Calling `std::fs` methods without checking `fs_scope()` will compile but may violate security policies and cause runtime errors in production builds.

## Implementing Custom Filesystem Commands

When adding a new command, follow this pattern: declare the function in a Rust module (e.g., [`src/lib.rs`](https://github.com/opengeos/GeoLibre/blob/main/src/lib.rs) or a dedicated file), annotate it with `#[tauri::command]`, make it `async` if performing I/O, and return a JSON-serializable type.

### Reading Text Files

This example demonstrates the fundamental pattern for reading files safely:

```rust
#[tauri::command]
pub async fn read_text_file(path: String, app: tauri::AppHandle) -> Result<String, String> {
    // Ensure the user granted access to this path
    if !app.fs_scope().is_allowed(&path) {
        return Err("Path not allowed by fs scope".into());
    }
    std::fs::read_to_string(&path).map_err(|e| format!("Failed to read file: {e}"))
}

```

The frontend invokes this via `invoke('read_text_file', { path })`.

### Handling Binary Data

For binary files, convert the bytes to a transferable format like base64:

```rust
#[tauri::command]
pub async fn read_binary_file(path: String, app: tauri::AppHandle) -> Result<String, String> {
    if !app.fs_scope().is_allowed(&path) {
        return Err("Access denied".into());
    }
    let bytes = std::fs::read(&path).map_err(|e| format!("IO error: {e}"))?;
    Ok(base64::encode(&bytes))
}

```

### Writing Structured Data

When writing JSON data, serialize the value before writing:

```rust
#[tauri::command]
pub async fn write_json_file(path: String, json: serde_json::Value, app: tauri::AppHandle) -> Result<(), String> {
    if !app.fs_scope().is_allowed(&path) {
        return Err("Access denied".into());
    }
    let text = serde_json::to_string_pretty(&json).map_err(|e| format!("Serialize error: {e}"))?;
    std::fs::write(&path, text).map_err(|e| format!("Write error: {e}"))
}

```

### Listing Directory Contents

To list directory entries while filtering for valid Unicode names:

```rust
#[tauri::command]
pub async fn list_dir(path: String, app: tauri::AppHandle) -> Result<Vec<String>, String> {
    if !app.fs_scope().is_allowed(&path) {
        return Err("Access denied".into());
    }
    let entries = std::fs::read_dir(&path)
        .map_err(|e| format!("Read dir error: {e}"))?;
    let mut names = Vec::new();
    for entry in entries {
        let entry = entry.map_err(|e| format!("Dir entry error: {e}"))?;
        if let Some(name) = entry.file_name().to_str() {
            names.push(name.to_string());
        }
    }
    Ok(names)
}

```

## Error Handling and Async Patterns

**Async execution** is essential for filesystem commands. Mark your functions as `async` to prevent blocking the Tauri runtime. Tauri automatically offloads this work to a background thread pool.

**Error handling** should convert `std::io::Error` into user-friendly `String` messages. This ensures the JavaScript side receives clear, actionable error descriptions rather than raw Rust debug output.

**Cross-platform paths** require using `PathBuf` and `std::path::Path` utilities instead of hardcoded separators. This guarantees consistent behavior across Windows, macOS, and Linux.

## Key Source Files in the Repository

- **[`apps/geolibre-desktop/src-tauri/src/lib.rs`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src-tauri/src/lib.rs)** — Central location where most custom Tauri commands are defined and where the `fs` plugin is initialized (see lines 43‑64). Also contains the scope verification pattern (lines 485‑487).

- **[`apps/geolibre-desktop/src-tauri/src/native_duckdb.rs`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src-tauri/src/native_duckdb.rs)** — Example module demonstrating conditional command exposure for vector file operations (`count_native_vector_file_features`, `load_native_vector_file`).

- **[`apps/geolibre-desktop/src-tauri/src/earth_engine_oauth.rs`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src-tauri/src/earth_engine_oauth.rs)** — Reference implementation showing async commands that interact with external services; useful for modeling long-running tasks.

- **[`apps/geolibre-desktop/src-tauri/src/main.rs`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src-tauri/src/main.rs)** — Application entry point that registers plugins (including `tauri_plugin_fs`) and launches the UI.

## Summary

- **Scope verification** is mandatory—always call `app.fs_scope().is_allowed(&path)` before any `std::fs` operation to respect Tauri's security sandbox.
- **Command structure** requires the `#[tauri::command]` attribute, `async` functions for I/O, and `Result<T, String>` return types.
- **Error handling** converts Rust errors into descriptive strings for the frontend.
- **Registration** happens automatically at compile time for any function with the `#[tauri::command]` attribute.
- **Key files** for reference include [`src/lib.rs`](https://github.com/opengeos/GeoLibre/blob/main/src/lib.rs) for command definitions and [`src/main.rs`](https://github.com/opengeos/GeoLibre/blob/main/src/main.rs) for plugin initialization.

## Frequently Asked Questions

### Do I need to manually register custom commands in the Tauri builder?

No. As implemented in opengeos/GeoLibre, any function annotated with `#[tauri::command]` is automatically exposed to the JavaScript frontend when the Tauri application is built. You do not need to explicitly register each command in [`main.rs`](https://github.com/opengeos/GeoLibre/blob/main/main.rs) or the builder pattern.

### How do I handle large file reads without blocking the UI?

Mark your command as `async` and use `std::fs` operations directly within the async function. Tauri will offload the blocking I/O to a background thread pool automatically. For extremely large files, consider wrapping the operation in `tauri::async_runtime::spawn_blocking` and streaming the data in chunks.

### Why does my command fail with "Path not allowed" even though the file exists?

The `tauri-plugin-fs` enforces a strict sandbox. You must verify the path using `app.fs_scope().is_allowed(&path)` before accessing it. Additionally, ensure the path has been explicitly granted access through the Tauri configuration or dialog permissions, as the plugin restricts access to only user-approved directories.

### Can I use the official fs plugin instead of writing custom commands?

Yes, but custom commands provide greater flexibility. The official `tauri-plugin-fs` offers standard operations, while custom commands allow you to implement complex logic—such as base64 encoding, JSON serialization, or directory filtering—while maintaining the same security model. GeoLibre uses both approaches: the plugin for basic operations and custom commands for specialized geospatial file handling.