File Operations
How to copy, move, rename, and delete files using the Tauri file system plugin with a React and Vite frontend.
Reading and writing files gives your app the ability to create and modify content, but a complete file system toolkit needs the ability to organize that content. Copying, moving, renaming, and deleting files are the operations that let you manage a user's local data as naturally as a desktop application would. In Tauri v2, the @tauri-apps/plugin-fs plugin provides a set of functions that handle these tasks while respecting the platform's security boundaries and the app's declared capabilities.
This page covers the practical use of copyFile, renameFile, and removeFile from the plugin. You will see how to configure permissions, handle errors, and chain operations together safely. The examples use a React frontend built with Vite, but the plugin works with any framework that can call Tauri's JavaScript APIs.
Permissions for File Operations
Every file operation that modifies the file system requires the fs:scope permission to include the paths you intend to touch. Without this, calls from the frontend will reject with a permission error. The scope is defined in a capability file inside src-tauri/capabilities/.
Here is a capability that grants access to the entire application data directory—a safe sandbox for file organization tasks.
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$APPDATA/**" }
]
}
]
}
Restricted Paths on Some Platforms:
On macOS and Linux, the $RESOURCES directory is read-only when the app is installed as a system package. On Android and iOS, access is limited to the app's own container. If your operations need to touch user-chosen folders outside these confines, you must use the Dialog plugin to let the user select a directory and then add that path to the scope at runtime.
The plugin must also be registered in your Rust backend. If you followed the plugin setup earlier in this chapter, the tauri-plugin-fs initialization is already present in lib.rs. If not, ensure the following is in place:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
With permissions configured and the plugin initialized, all file operation functions imported from @tauri-apps/plugin-fs will respect the declared scope.
Copying Files
Copying a file creates an identical duplicate at a destination path. The original file remains untouched. The plugin provides copyFile(source, destination, options?) for this. Both source and destination can be relative to a base directory specified in the options, or they can be absolute paths when the scope allows them.
A copy operation will fail if the destination file already exists, unless you explicitly set options.fromPathOverwrite to true. This protects the user from accidental data loss.
The example below copies a user's settings backup file from the app data directory to a dedicated backups subfolder. It creates the subfolder if it does not already exist, then performs the copy and displays a success message.
import { useState } from "react";
import {
copyFile,
exists,
mkdir,
BaseDirectory,
} from "@tauri-apps/plugin-fs";
function App() {
const [status, setStatus] = useState("");
const handleCopy = async () => {
try {
const backupDir = "backups";
const sourceFile = "settings.json";
const destFile = "backups/settings_backup.json";
if (!(await exists(backupDir, { baseDir: BaseDirectory.AppData }))) {
await mkdir(backupDir, {
baseDir: BaseDirectory.AppData,
recursive: true,
});
}
await copyFile(sourceFile, destFile, {
baseDir: BaseDirectory.AppData,
fromPathOverwrite: true,
});
setStatus("File copied successfully.");
} catch (error) {
setStatus(`Copy failed: ${error}`);
}
};
return (
<div>
<button onClick={handleCopy}>Copy Settings Backup</button>
<p>{status}</p>
</div>
);
}
export default App;
copyFile reads the source file and writes its contents to the destination. On the frontend, the call returns a promise that resolves when the operation finishes. If an error occurs—such as the source file not existing or the destination path being outside the permitted scope—the promise rejects with a descriptive message.
Overwriting Without Confirmation:
Setting fromPathOverwrite: true replaces the destination file silently. Always confirm with the user before overwriting anything that might contain unsaved work.
A common misconception is that copyFile handles directories. It does not. The plugin provides copyFile only for individual files. To duplicate an entire directory tree, you must recursively create subdirectories and copy each file yourself, or use a backend Rust command with std::fs.
Moving and Renaming Files
The plugin uses a single function, renameFile(oldPath, newPath, options?), for both renaming and moving. If the old and new paths share the same parent directory, the file is renamed. If the new path points to a different directory, the file is moved there—effectively cutting it from the old location and placing it in the new one.
This unified API matches how operating systems treat these actions internally: a rename is just a move within the same directory entry table.
The following component renames a file when both paths share a folder, then moves it to a subdirectory in a second step.
import { useState } from "react";
import { renameFile, exists, BaseDirectory } from "@tauri-apps/plugin-fs";
function App() {
const [message, setMessage] = useState("");
const renameAndMove = async () => {
try {
const oldName = "notes/draft.txt";
const newName = "notes/final.txt";
const movedName = "notes/archive/final.txt";
if (!(await exists(oldName, { baseDir: BaseDirectory.AppData }))) {
setMessage("Source file does not exist.");
return;
}
// Rename within the same directory
await renameFile(oldName, newName, {
baseDir: BaseDirectory.AppData,
});
// Move the renamed file to a subdirectory
await renameFile(newName, movedName, {
baseDir: BaseDirectory.AppData,
});
setMessage("File renamed and moved successfully.");
} catch (error) {
setMessage(`Operation failed: ${error}`);
}
};
return (
<div>
<button onClick={renameAndMove}>Rename and Move File</button>
<p>{message}</p>
</div>
);
}
export default App;
The first call changes the name from draft.txt to final.txt inside the notes folder. The second call moves that renamed file into the notes/archive folder. Because both operations use renameFile, the code is concise and the intent is clear.
Overwrite Behavior for Rename/Move:
If the destination path already exists, renameFile will overwrite it on most platforms. This is different from copyFile, which requires fromPathOverwrite: true. Always check with exists before renaming to avoid losing data.
Beginners often wonder whether moving a file across different mount points or drives works. On desktop platforms, renameFile uses the operating system's atomic rename call, which usually fails when the source and destination are on different filesystems. In that case, you must fall back to a manual copy followed by a delete. The plugin does not handle this automatically.
Deleting Files
To remove a file permanently from the file system, use removeFile(path, options?). There is no trash or recycle bin integration—the file is gone immediately. This makes the operation lightweight but also dangerous if called on the wrong path.
The component below deletes a temporary cache file after confirming with the user.
import { useState } from "react";
import { removeFile, exists, BaseDirectory } from "@tauri-apps/plugin-fs";
function App() {
const [feedback, setFeedback] = useState("");
const deleteCacheFile = async () => {
const target = "cache/temp_data.bin";
const existsCheck = await exists(target, {
baseDir: BaseDirectory.AppData,
});
if (!existsCheck) {
setFeedback("Cache file not found. Nothing to delete.");
return;
}
const confirmed = window.confirm(
"Permanently delete the cache file? This cannot be undone.",
);
if (!confirmed) return;
try {
await removeFile(target, { baseDir: BaseDirectory.AppData });
setFeedback("Cache file deleted.");
} catch (error) {
setFeedback(`Deletion failed: ${error}`);
}
};
return (
<div>
<button onClick={deleteCacheFile}>Delete Cache File</button>
<p>{feedback}</p>
</div>
);
}
export default App;
removeFile will reject if the path is a directory. To delete a directory, the plugin provides removeDir (covered in the Working with Directories section). Attempting to delete a file that does not exist also causes a rejection, so the example checks existence beforehand.
Guard With Confirmation Dialogs:
Using window.confirm (or a custom UI modal) before calling removeFile is a pattern that prevents accidental data loss. Even if you skip the dialog for internal cache files, always log what is being deleted so you can recover from mistakes during development.
A subtle platform detail: on Windows, deleting a file that another process has open will fail with a permission error. On Linux and macOS, the file's name is removed from the directory but the data persists until the last file handle is closed. This means a deletion on those systems can appear to succeed while the storage space is not yet freed. For most use cases this difference is invisible, but if your app needs to verify that space has been reclaimed, you cannot rely on a successful removeFile call alone.
A Combined Workflow: Backup Then Remove
Real applications rarely perform a single file operation in isolation. A backup script, for example, copies a file to a safe location, verifies the copy, and then deletes the original if the backup is intact. The following stepper walks through that exact sequence, using the functions you have already seen.
Ensure the backup directory exists
Before copying anything, create the backups folder if it is missing. This prevents the copy step from failing because the destination directory is absent.
const backupDir = "backups";
if (!(await exists(backupDir, { baseDir: BaseDirectory.AppData }))) {
await mkdir(backupDir, { baseDir: BaseDirectory.AppData, recursive: true });
}
Copy the file to the backup location
Duplicate the target file into the backup directory. Overwrite any previous backup.
await copyFile("data/current.log", "backups/current.log.bak", {
baseDir: BaseDirectory.AppData,
fromPathOverwrite: true,
});
Verify the backup copy exists and has the correct size
Check that the copy succeeded. Use the stat function from the plugin to retrieve the file sizes and compare them. If the sizes differ, stop and alert the user.
import { stat } from "@tauri-apps/plugin-fs";
const originalStat = await stat("data/current.log", {
baseDir: BaseDirectory.AppData,
});
const backupStat = await stat("backups/current.log.bak", {
baseDir: BaseDirectory.AppData,
});
if (originalStat.size !== backupStat.size) {
throw new Error("Backup verification failed: size mismatch.");
}
Delete the original file
With the backup confirmed, remove the source file. This is the moment the file truly moves out of its original location.
await removeFile("data/current.log", { baseDir: BaseDirectory.AppData });
Each step depends on the previous one succeeding. If verification fails, the original file remains untouched. This pattern is the foundation of safe file management in any application that cannot afford data loss.
Atomicity Is Not Guaranteed:
The sequence above is not atomic. If the app crashes between step 2 and step 4, both the original and the backup exist, which is safe but leaves clutter. For truly atomic moves, use renameFile directly on the same filesystem—it delegates to the OS's atomic rename call.
What You Have Learned
The four operations covered here—copy, rename/move, delete—form the vocabulary your app needs to manage files beyond simple reads and writes. The plugin's API keeps these calls straightforward while the permission system ensures the app cannot wander into directories the user did not authorize.
If you plan to build a file manager, a backup tool, or any feature that touches dozens of files at once, those patterns will keep your code reliable.