How to implement file management in a Tauri-based Android app.
Introduction
Recently, I decided to revive an old project of mine—a personal savings journal. Initially built with Next.js and MongoDB Atlas, this setup required database maintenance and hosting, which felt excessive for such a small application. Additionally, sharing it with the community was challenging, leaving me as the sole user.
My new idea is to develop an offline, mobile-first app that stores all user data locally. This approach eliminates the need for infrastructure maintenance and allows for easy distribution as an Android app (an iOS developer account is too costly for a non-profitable, open-source app).
My research led me to the Tauri framework, a lightweight alternative to Electron. To finalize my tech stack decision, I started by implementing the riskiest and least familiar feature: saving and reading data on an Android device. During this exploration, I wrote an article and created a playground app for experimenting with file management in Tauri.
Tauri Overview
Tauri is a cross-platform framework similar to Electron. Its main advantage is the use of native web views, which reduces the package size significantly—around 30MB for a simple React app. However, this might present challenges for more complex applications. As a JavaScript developer, the primary drawback for me is Tauri's Rust backend, making it tricky to write backend logic. Fortunately, my app only requires saving and reading a local database, so I decided to give it a try.
Setting Up a Project
Starting a new project was straightforward. Refer to the official documentation or check out my playground repo. The most challenging part was setting up PATH variables and installing additional Android SDK packages after installing android-studio
:
- Install
ndk
withinandroid-studio
: Settings > Languages & Frameworks > SDK Tools. - Set the
NDK_HOME
andANDROID_HOME
PATH variables (ANDROID_HOME
is visible in Settings > Languages & Frameworks):
export ANDROID_HOME=YOUR_PATH/Android/sdk
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH
export NDK_HOME=$ANDROID_HOME/ndk/29.0.13113456
export PATH=$PATH:$NDK_HOME
- Initialize the Android project:
pnpm tauri android init
After completing these steps, pnpm tauri android dev
worked seamlessly, launching the Android emulator or using a connected phone.
Tauri File Management Plugins
Tauri offers several official plugins for file system manipulation:
The file-system
plugin is designed for silently reading and writing files, while the dialog
plugin explicitly asks the user to pick a file using the system file explorer.
Tauri Capabilities
Configuring permissions for plugins was the most frustrating part. Tauri limits permissions by default, requiring manual specification of locations and permissions for the file-system
plugin. Restarting the dev server was necessary after changing capabilities, and error messages were often unclear, making it difficult to determine whether the issue was with capabilities configuration or Android permissions.
Silently Managing Files
In my playground project, I created <ReadFileCard>
, <ReadFilePath>
, and <WriteFileCard>
components to experiment with file management. Reading and writing files is straightforward:
import { readTextFile } from "@tauri-apps/plugin-fs";
async function readTxtFile(fileName: string) {
const file = await readTextFile(fileName);
// or using BaseDirectory (look at docs)
return file;
}
The baseDirectory
mapping to the Android file system is not obvious. Some paths map as follows:
-
BaseDirectory.Home
points to the external shared directory:/storage/emulated/0
-
BaseDirectory.LocalData
,BaseDirectory.AppData
, and others point to internal private storage:/data/user/0/com.myapp.app
-
BaseDirectory.Document
points to internal shared storage:/storage/emulated/0/Android/data/com.myapp.app/documents
As a user, I could only access /storage/emulated/0/*
with the built-in file manager, while /storage/emulated/0/Android/data/
showed no paths. However, it was accessible via adb shell
.
Android Limitations
You cannot create files or folders directly in ExternalStorage
. Instead, use directories like Documents
, Download
, Pictures
, or Videos
. For example, use /storage/emulated/0/Documents/MyFolder/file.txt
instead of /storage/emulated/0/MyFolder/file.txt
.
On Android 10 and above, you can create files and folders in these directories without permissions, but you cannot read files created by other apps. In this case, use a dialog
to read the files.
For Android 9 and earlier, the app must explicitly ask for user permission to access files in Documents
, Downloads
, etc. To access files in /storage/emulated/0/
on any Android version, the app must explicitly ask for user permission.
Currently, Tauri does not provide a plugin for requesting user permission to access external storage. Therefore, I changed the minimum supported Android version to 10:
// src-tauri/tauri.conf.json
"bundle": {
+ "android": {
+ "minSdkVersion": 29
+ }
}
Capabilities
To work with /storage/emulated/0/Documents/*
, configure the capabilities as follows:
// src-tauri/capabilities/default.json
{
"identifier": "fs:allow-write",
"allow": [
{
"path": "$HOME/Documents/*"
}
]
},
{
"identifier": "fs:allow-create",
"allow": [
{
"path": "$HOME/Documents/*"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$HOME/Documents/*"
}
]
}
Explicitly Managing Files
In my playground project, I created <ReadExtFileCard>
and <WriteExtFileCard>
components to experiment with file management using the dialog
plugin. This plugin does not require additional permissions and allows access to any file the user selects in the file explorer. For example, to read a file using both dialog
and file-system
plugins:
import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";
...
onClick={async (e) => {
try {
const directory = await loadExtFile(); // `dialog` returns selected path
const content = await readTxtFile(directory); // Now file system plugin has permissions to selected path
setState(`File: ${directory} content: ${content}`);
} catch (error) {
setState(error as string);
}
}}
...
async function loadExtFile() {
const path = await open({
multiple: false,
directory: false,
});
return path;
}
async function readTxtFile(fileName: string) {
const fileContent = await readTextFile(fileName);
return fileContent;
}
Check out playground app: https://github.com/skorphil/tauri-fs-android-starter
Happy coding!
Top comments (0)