Working with Directories
Learn how to create, read, delete, and check existence of directories in Tauri v2 using the File System plugin.
Most applications need to organize data into folders — configs, logs, user-generated content, caches. The Tauri file system plugin provides a set of directory operations that work across desktop and mobile platforms while respecting your app’s security scope. This section covers everything you need to reliably create, list, delete, and verify directories from your frontend code or from Rust backend commands.
Before You Start
Directory operations build on the same plugin and permission model as file operations. If you haven’t installed @tauri-apps/plugin-fs and initialized the Rust plugin, complete the File System API introduction first. The key point for directories is that every operation — mkdir, readDir, remove — requires its own explicit permission in your capability file.
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-exists",
"fs:allow-mkdir",
"fs:allow-readdir",
"fs:allow-remove",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$APPDATA/**" }
]
}
]
}
Missing permissions cause silent failures:
If your app calls mkdir without fs:allow-mkdir in the capability file, Tauri will reject the request with a “forbidden path” error. The same applies to readDir and remove. Always grant the exact operation permissions you intend to use, not just fs:default.
Checking if a Directory Exists
Before you create a folder or try to read its contents, you often need to know whether it already exists. The plugin’s exists function works for both files and directories — it returns true if anything with that name exists at the given path.
import { exists, BaseDirectory } from '@tauri-apps/plugin-fs';
async function checkConfigDir(): Promise<boolean> {
const dirExists = await exists('config', {
baseDir: BaseDirectory.AppData,
});
return dirExists;
}
The JavaScript version uses a baseDir — the path 'config' is relative to the app data folder. If you need to check an absolute path (for example, a folder the user selected through a dialog), the Rust approach with std::fs is the way to go, because the plugin’s JS API can only reach paths inside its declared scopes.
exists() does not distinguish file from directory:
If a file named config exists at that location, exists still returns true. To differentiate, you’d need to use readDir or a Rust is_dir() check.
Creating Directories
The mkdir function creates a new directory. By default, it only creates the final component of the path — if any intermediate directories are missing, the call fails. Setting recursive: true makes it create all missing parent directories automatically, similar to mkdir -p in a terminal.
import { mkdir, BaseDirectory } from '@tauri-apps/plugin-fs';
await mkdir('projects/2025/photos', {
baseDir: BaseDirectory.AppData,
recursive: true,
});
This creates $APPDATA/projects/2025/photos even if neither projects nor 2025 existed before. Without recursive: true, the call would throw an error because the intermediate directories don’t exist.
Verify your setup:
Run the above snippet in a dev build, then inspect your app’s data folder. On Windows you’ll find it under C:\Users\YourName\AppData\Roaming\com.tauri.app. If the nested folders appear, directory creation is working correctly.
The Rust equivalent using std::fs can bypass the plugin’s scope entirely — useful when you need to create a folder at a user‑chosen path from a dialog.
use std::fs;
#[tauri::command]
fn create_dir(path: String) -> Result<(), String> {
fs::create_dir_all(&path).map_err(|e| e.to_string())
}
Use this approach only after the user has explicitly selected a location through a dialog. Otherwise, an arbitrary path could run into OS permission errors on macOS and Linux.
Recursive is not the same as safe:
recursive: true won’t protect you from creating directories outside your scope. If your scope only allows $APPDATA/** but you construct an absolute path like /tmp, the call still fails. The scope is the gatekeeper, not the recursive option.
Reading Directory Contents
readDir returns an array of entries, each containing the entry’s name, whether it’s a file or a directory, and sometimes metadata like size (for files). It reads the immediate children — not a recursive tree.
import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs';
async function listAppRoot() {
const entries = await readDir('', {
baseDir: BaseDirectory.AppData,
});
for (const entry of entries) {
console.log(
`${entry.name} — ${entry.isDirectory ? 'folder' : 'file'}`
);
}
}
The JavaScript API respects the scope — you can only list directories that fall under $APPDATA (or whatever you’ve permitted). The Rust version with std::fs::read_dir has no such limitation, making it ideal for browsing a user‑selected folder from the dialog plugin.
If you need to build a tree view — listing all sub‑directories and their contents — you’ll write a recursive function that calls readDir on every entry marked isDirectory. Remember that deep recursion on large directory trees can be slow; consider adding a depth limit in production apps.
Deleting Directories
The remove function deletes a file or directory. To delete a directory that still contains files or sub‑directories, you must pass recursive: true. Without it, the call throws an error and the directory stays intact.
import { remove, BaseDirectory } from '@tauri-apps/plugin-fs';
// Remove an empty folder
await remove('temp', { baseDir: BaseDirectory.AppData });
// Remove a non‑empty folder
await remove('old-cache', {
baseDir: BaseDirectory.AppData,
recursive: true,
});
Recursive deletion is permanent:
There is no trash bin — once you call remove with recursive: true, the folder and all its contents are gone. Always confirm the operation with the user or back up critical data first.
For user‑selected directories, a Rust command using std::fs::remove_dir_all provides the same functionality but operates on absolute paths returned by the dialog plugin.
use std::fs;
#[tauri::command]
fn delete_dir(path: String) -> Result<(), String> {
fs::remove_dir_all(&path).map_err(|e| e.to_string())
}
Common Mistakes and How to Avoid Them
Several recurring pitfalls catch developers who are new to Tauri’s security model.
Forgetting the recursive flag on mkdir:
Calling mkdir('a/b') without recursive: true throws a “path not found” error if a doesn’t exist. The error message sometimes misleads you into thinking the path is forbidden, when it’s actually a missing parent directory.
Using relative paths outside the scope:
Paths like ../../something are outright blocked by the plugin’s path traversal protection. If you need to access a location that can’t be expressed relative to a base directory, use a Rust command with std::fs and let the user pick the path through a dialog.
Confusing scope patterns:
A pattern like $APPDATA allows access to the top‑level app data folder, but not its children. To grant recursive access, use $APPDATA/**. Missing the /** suffix is one of the most common permission misconfigurations.
Platform differences:
On Linux and macOS, the $RESOURCES directory is read‑only. If your app needs to create or modify folders inside it during runtime, store them under $APPDATA or $CACHE instead.
Real-World Example: A Minimal File Explorer Component
This React component ties together all four operations. It lists the contents of the app’s notes folder, lets the user create a new subfolder, and delete existing ones. It uses the JavaScript plugin API and targets BaseDirectory.AppData.
import { useState, useEffect } from 'react';
import {
readDir,
mkdir,
remove,
exists,
BaseDirectory,
} from '@tauri-apps/plugin-fs';
interface DirEntry {
name: string;
isDirectory: boolean;
}
export default function NoteExplorer() {
const [entries, setEntries] = useState<DirEntry[]>([]);
const [newFolder, setNewFolder] = useState('');
const basePath = 'notes';
const refresh = async () => {
const dirExists = await exists(basePath, {
baseDir: BaseDirectory.AppData,
});
if (!dirExists) {
await mkdir(basePath, {
baseDir: BaseDirectory.AppData,
recursive: true,
});
}
const result = await readDir(basePath, {
baseDir: BaseDirectory.AppData,
});
setEntries(result as DirEntry[]);
};
useEffect(() => {
refresh();
}, []);
const handleCreateFolder = async () => {
if (!newFolder.trim()) return;
await mkdir(`${basePath}/${newFolder}`, {
baseDir: BaseDirectory.AppData,
recursive: true,
});
setNewFolder('');
await refresh();
};
const handleDelete = async (name: string) => {
const confirmed = window.confirm(`Delete folder “${name}” and all its contents?`);
if (!confirmed) return;
await remove(`${basePath}/${name}`, {
baseDir: BaseDirectory.AppData,
recursive: true,
});
await refresh();
};
return (
<div>
<h2>My Notes</h2>
<ul>
{entries
.filter((e) => e.isDirectory)
.map((entry) => (
<li key={entry.name}>
📁 {entry.name}
<button onClick={() => handleDelete(entry.name)}>
Delete
</button>
</li>
))}
</ul>
<div>
<input
value={newFolder}
onChange={(e) => setNewFolder(e.target.value)}
placeholder="New folder name"
/>
<button onClick={handleCreateFolder}>Create Folder</button>
</div>
</div>
);
}
The component first checks if the notes base directory exists; if not, it creates one. Then it reads the contents, displaying only sub‑directories. Creating a folder triggers a refresh so the list stays current. Deleting a folder asks for confirmation and uses recursive: true to cleanly remove everything inside.
Everything is wired up:
Once the capability file includes fs:allow-mkdir, fs:allow-readdir, fs:allow-remove, and a scope of $APPDATA/**, this component should work out of the box. If you see the “notes” folder and any sub‑folders you create, directory operations are fully functional.
Summary
Directory management in Tauri v2 revolves around a small set of intuitive functions — exists, mkdir, readDir, remove — but their correct behavior depends entirely on the permissions you declare and the path scope you allow. The plugin’s JavaScript API keeps your app secure by restricting operations to approved locations, while Rust commands with std::fs give you the escape hatch for user‑chosen paths that fall outside any predefined scope.
The foundation you've built here — understanding scopes, recursion, and permission tokens — carries forward into every part of Tauri’s native APIs.