Platform-specific file system paths for Tauri 2.x apps.
Provides platform-specific file system path resolution with 1:1 parity to each OS's native directory APIs.
Tauri's built-in path APIs (and the underlying dirs crate) apply a
cross-platform abstraction that can produce incorrect or inconsistent paths on
mobile. For example, on iOS the dirs crate applies macOS
conventions — appending /Library/Application Support/<bundle-id> to the
sandbox root — rather than using FileManager APIs to resolve the correct
sandbox-relative path. Similar inconsistencies appear on Android, where four
distinct directory types can resolve to the same location.
This matters because mobile platforms are opinionated about directory structure
within the app sandbox, and the correct choice of directory has real
consequences: on iOS, files in Documents/ are included in device backups and
visible in the Files app, while Library/Application Support/ is backed up but
hidden. On Android, scoped storage rules and manufacturer-specific behavior
affect where media and app data should be persisted.
This plugin bypasses the abstraction layer and resolves paths directly through
each platform's native APIs (FileManager on Apple, Context on Android,
Known Folders / ApplicationData on Windows), so the resolved path always
matches what the OS itself would return.
This plugin also supports paths for Windows apps packaged as both MSI and MSIX. MSI paths are supported with Win32Paths, and MSIX paths with WindowsApplicationDataPaths.
| Platform | Supported |
|---|---|
| Linux | ✓ |
| Windows | ✓ |
| macOS | ✓ |
| Android¹ | ✓ |
| iOS² | ✓ |
-
Install NPM dependencies:
npm install
-
Build the TypeScript bindings:
npm run build
-
Build the Rust plugin:
cargo build
Run Rust tests:
cargo testRun Typescript tests:
npm run testThis plugin requires a Rust version of at least 1.94.0
Add the plugin to your Cargo.toml:
src-tauri/Cargo.toml
[dependencies]
tauri-plugin-fs-resolver = { git = "https://github.com/silvermine/tauri-plugin-fs-resolver" }Install the JavaScript bindings:
npm install @silvermine/tauri-plugin-fs-resolverInitialize the plugin in your tauri::Builder:
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_fs_resolver::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}The plugin exposes two levels of API:
-
PathMapping— a cross-platform path definition that maps each platform to its correct path enum variant. Define the mapping once, then callresolve()at runtime; the current platform is detected automatically and the corresponding resolve function is called. If the current platform has no entry in the mapping, an error is returned. This is the recommended entry point for most use cases. -
Platform-specific resolve functions — each function targets a single platform and accepts that platform's path enum. These are the low-level building blocks. Calling a resolve function on the wrong platform throws an error (TypeScript) or returns an
Err(Rust).
The Rust and TypeScript APIs are shaped differently to fit each language's idioms while sharing the same IPC commands underneath.
Rust exposes a single PathResolver struct. It owns the current OS
string, holds the platform-specific resolve functions, and provides methods
for both cross-platform mapping resolution (resolve_mapping) and direct
per-platform resolution (resolve_ios, resolve_mac, resolve_android,
resolve_windows, resolve_android_path_collection). Callers construct it
once with PathResolver::new() — no arguments needed — and use that
instance for all resolution.
TypeScript exposes individual async functions (resolveIosPath,
resolveMacPath, resolveAndroidPath, resolveWindowsPath,
resolveAndroidPathCollection) that each call a corresponding Tauri IPC command, plus a
PathMapping class for cross-platform resolution. Because Tauri IPC can only invoke flat
commands (not methods on a Rust struct), the TypeScript layer does not mirror the
PathResolver struct directly — the individual functions are the natural binding to the
IPC surface.
Platform gating: Both layers check the current OS before making a resolve call. The TypeScript functions check
platform()before callinginvoke()to avoid an unnecessary IPC round trip when running on the wrong platform. The Rust side also validates the OS, so the check is enforced regardless of how the command is invoked.
Use PathMapping when your app targets multiple platforms and you want a
single definition that resolves to the right directory on each one. All fields
are optional — only provide entries for the platforms you ship on.
This is the main intended usage for this library. The goal is for developers to define once what the platform-specific paths should be, and the library will handle resolving to the path of the current OS.
In other words, callers don't need to write any platform-branching logic themselves unless they want to.
JavaScript / TypeScript
import { PathMapping } from '@silvermine/tauri-plugin-fs-resolver';
import {
IosPath,
MacPath,
AndroidPath,
LinuxPath,
Win32Path,
} from '@silvermine/tauri-plugin-fs-resolver/types';
const tempDir = new PathMapping({
android: AndroidPath.CacheDir,
ios: IosPath.CachesDirectory,
linux: LinuxPath.CacheHome,
macos: MacPath.CachesDirectory,
windows: { win32: Win32Path.LocalAppData },
});
const downloadsDir = new PathMapping({
ios: IosPath.DownloadsDirectory,
macos: MacPath.DownloadsDirectory,
linux: LinuxPath.DownloadDir,
android: AndroidPath.ExternalFilesDirectoryDownloads,
windows: { win32: Win32Path.Downloads },
});
// These methods are what should actually be called by the rest of the app.
export async function getTempDir() {
return await tempDir.resolve()
}
export async function getDownloadsDir() {
return await downloadsDir.resolve()
}Rust
use fs_resolver::{PathResolver, PathMapping, IosPath, LinuxPath, MacPath, AndroidPath, WindowsPath, Win32Path, Error};
let resolver = PathResolver::new();
let temp_dir = PathMapping {
android: Some(AndroidPath::CacheDir),
ios: Some(IosPath::CachesDirectory),
linux: Some(LinuxPath::CacheHome),
macos: Some(MacPath::CachesDirectory),
windows: Some(WindowsPath::Win32(Win32Path::LocalAppData)),
};
let downloads_dir = PathMapping {
android: Some(AndroidPath::ExternalFilesDirectoryDownloads),
ios: Some(IosPath::DownloadsDirectory),
linux: Some(LinuxPath::DownloadDir),
macos: Some(MacPath::DownloadsDirectory),
windows: Some(WindowsPath::Win32(Win32Path::Downloads)),
};
// These methods are what should actually be called by the rest of the app.
pub fn temp_dir() -> Result<PathBuf> {
resolver.resolve_mapping(&temp_dir)
}
pub fn downloads_dir() -> Result<PathBuf> {
resolver.resolve_mapping(&downloads_dir)
}For cases where you only need a single platform, or need finer control than
PathMapping provides (e.g. resolving a path collection on Android), use the
resolve functions directly.
JavaScript / TypeScript
All resolve functions are async and platform-gated — calling one on the wrong platform throws an error immediately without an IPC round trip.
// Android
import {
resolveAndroidPath,
resolveAndroidPathCollection,
} from '@silvermine/tauri-plugin-fs-resolver';
import {
AndroidPath,
AndroidPathCollection,
} from '@silvermine/tauri-plugin-fs-resolver/types';
const cacheDir = await resolveAndroidPath(AndroidPath.CacheDir);
const filesDir = await resolveAndroidPath(AndroidPath.FilesDir);
const pictures = await resolveAndroidPath(AndroidPath.ExternalFilesDirectoryPictures);
const allExternalCaches = await resolveAndroidPathCollection(
AndroidPathCollection.ExternalCacheDirs,
);
const allMediaDirs = await resolveAndroidPathCollection(
AndroidPathCollection.ExternalMediaDirs,
);// iOS
import { resolveIosPath } from '@silvermine/tauri-plugin-fs-resolver';
import { IosPath } from '@silvermine/tauri-plugin-fs-resolver/types';
const library = await resolveIosPath(IosPath.LibraryDirectory);
const appSupport = await resolveIosPath(IosPath.ApplicationSupportDirectory);
const caches = await resolveIosPath(IosPath.CachesDirectory);
const documents = await resolveIosPath(IosPath.DocumentDirectory);
const downloads = await resolveIosPath(IosPath.DownloadsDirectory);// Linux
import { resolveLinuxPath } from '@silvermine/tauri-plugin-fs-resolver';
import { LinuxPath } from '@silvermine/tauri-plugin-fs-resolver/types';
const data = await resolveLinuxPath(LinuxPath.DataHome);
const caches = await resolveLinuxPath(LinuxPath.CacheHome);
const documents = await resolveLinuxPath(LinuxPath.DocumentDir);
const downloads = await resolveLinuxPath(LinuxPath.DownloadDir);// macOS
import { resolveMacPath } from '@silvermine/tauri-plugin-fs-resolver';
import { MacPath } from '@silvermine/tauri-plugin-fs-resolver/types';
const library = await resolveMacPath(MacPath.LibraryDirectory);
const appSupport = await resolveMacPath(MacPath.ApplicationSupportDirectory);
const caches = await resolveMacPath(MacPath.CachesDirectory);
const documents = await resolveMacPath(MacPath.DocumentDirectory);
const downloads = await resolveMacPath(MacPath.DownloadsDirectory);// Windows (Win32 / MSI)
import { resolveWindowsPath } from '@silvermine/tauri-plugin-fs-resolver';
import { Win32Path } from '@silvermine/tauri-plugin-fs-resolver/types';
const appData = await resolveWindowsPath({ win32: Win32Path.RoamingAppData });
const localAppData = await resolveWindowsPath({ win32: Win32Path.LocalAppData });
const documents = await resolveWindowsPath({ win32: Win32Path.Documents });// Windows (MSIX)
import { resolveWindowsPath } from '@silvermine/tauri-plugin-fs-resolver';
import { WindowsApplicationDataPath } from '@silvermine/tauri-plugin-fs-resolver/types';
const localFolder = await resolveWindowsPath({ winMsix: WindowsApplicationDataPath.LocalFolder });
const roamingFolder = await resolveWindowsPath({ winMsix: WindowsApplicationDataPath.RoamingFolder });
const tempFolder = await resolveWindowsPath({ winMsix: WindowsApplicationDataPath.TemporaryFolder });Rust
All resolution goes through a PathResolver instance. Each method validates
that the current OS matches the target platform and returns Result<PathBuf>.
use fs_resolver::{PathResolver, AndroidPath, IosPath, LinuxPath, MacPath, Win32Path, WindowsApplicationDataPath, WindowsPath};
let resolver = PathResolver::new();
// Android
let cache = resolver.resolve_android(&AndroidPath::CacheDir)?;
let files = resolver.resolve_android(&AndroidPath::FilesDir)?;
// iOS
let library = resolver.resolve_ios(&IosPath::LibraryDirectory)?;
let app_support = resolver.resolve_ios(&IosPath::ApplicationSupportDirectory)?;
let caches = resolver.resolve_ios(&IosPath::CachesDirectory)?;
// Linux
let config = resolver.resolve_linux(&LinuxPath::ConfigHome)?;
let desktop = resolver.resolve_linux(&LinuxPath::DesktopDir)?;
// MacOS
let library = resolver.resolve_mac(&MacPath::LibraryDirectory)?;
let app_support = resolver.resolve_mac(&MacPath::ApplicationSupportDirectory)?;
let caches = resolver.resolve_mac(&MacPath::CachesDirectory)?;
// Windows (Win32 / MSI)
let app_data = resolver.resolve_windows(&WindowsPath::Win32(Win32Path::RoamingAppData))?;
let documents = resolver.resolve_windows(&WindowsPath::Win32(Win32Path::Documents))?;
// Windows (MSIX)
let local_folder = resolver.resolve_windows(&WindowsPath::WinMsix(WindowsApplicationDataPath::LocalFolder))?;
let temp_folder = resolver.resolve_windows(&WindowsPath::WinMsix(WindowsApplicationDataPath::TemporaryFolder))?;| Platform | Resolution strategy |
|---|---|
| macOS | Native calls via objc2-foundation |
| iOS | Native calls via objc2-foundation |
| Linux | Rust std::env and XDG conventions |
| Windows | SHGetKnownFolderPath (Win32) or WinRT ApplicationData (MSIX) |
| Android | JNI bridge to Kotlin via Tauri PluginHandle |
On all platforms except Android, paths are resolved directly in Rust using native bindings or standard library APIs. No Tauri dependency is required at the resolver level for these platforms.
Android requires crossing the JNI boundary to call Context methods
(e.g. getFilesDir(), getExternalCacheDirs()) that are only
available in the Kotlin runtime. At plugin initialization, the Tauri
PluginHandle is captured in closures and injected into the
PathResolver, keeping the public API free of Tauri types on all
platforms.
Windows path resolution is a tagged union (WindowsPath) with two variants.
Callers choose the variant that matches how the app is packaged.
If the incorrect variant is used in the app, an exception is thrown.
For example, if the user attempts to resolve a Win32Path in an MSIX packaged
app (or vice versa), an exception will be thrown.
| Variant | Serde / TypeScript tag | Enum | Resolution API |
|---|---|---|---|
| Unpackaged Win32 / MSI | win32 |
Win32Path |
SHGetKnownFolderPath (KNOWNFOLDERID) |
| MSIX (package identity) | winMsix |
WindowsApplicationDataPath |
WinRT ApplicationData |
Win32Path covers standard desktop known folders (Documents, LocalAppData,
Downloads, etc.). Use this for unpackaged apps, MSI installs, and any Win32
process without package identity. Windows has no API to detect MSI specifically;
all unpackaged Win32 processes (including MSI) lack package identity and share
the same path model.
WindowsApplicationDataPath covers the five app-container folders exposed
by ApplicationData::Current() (LocalFolder, RoamingFolder,
LocalCacheFolder, TemporaryFolder, SharedLocalFolder). Use this when the
app ships as MSIX (Microsoft Store, sideloaded .msix, or loose-layout debug
runs with package identity). These paths live under
AppData\Local\Packages\<package-id>\… and are the correct locations for
app-private data in a sandboxed package.
At runtime the resolver validates that the chosen variant matches the process context:
- Resolving a
win32path while running inside an MSIX package returnsWin32PathInvokedFromMsixPackagedContext. - Resolving a
winMsixpath outside a package returnsWindowsApplicationDataPathInvokedFromWin32Context.
Package identity is detected by whether ApplicationData::Current() succeeds —
the same signal described in Microsoft's winapp CLI + Tauri
guide.
TypeScript
import { Win32Path, WindowsApplicationDataPath } from '@silvermine/tauri-plugin-fs-resolver/types';
// Unpackaged / MSI — known folders
const downloads = { win32: Win32Path.Downloads };
// MSIX — app-container folders
const appData = { winMsix: WindowsApplicationDataPath.LocalFolder };Rust
use fs_resolver::{WindowsPath, Win32Path, WindowsApplicationDataPath};
let downloads = WindowsPath::Win32(Win32Path::Downloads);
let app_data = WindowsPath::WinMsix(WindowsApplicationDataPath::LocalFolder);Linux path resolution follows the XDG Base Directory Specification and XDG User Directories.
LinuxPath returns base directories, not app-specific paths. Append your
app identifier after resolution (e.g. DataHome → ~/.local/share/<app-id>/).
| Category | Variants | Source |
|---|---|---|
| XDG base | DataHome, ConfigHome, CacheHome, StateHome, RuntimeDir, Home, ExecutableDir, FontDir |
$XDG_* env vars with spec-defined fallbacks |
| User dirs | DesktopDir, DocumentDir, DownloadDir, MusicDir, PictureDir, VideoDir, TemplateDir, PublicDir |
~/.config/user-dirs.dirs |
RuntimeDir ($XDG_RUNTIME_DIR) has no fallback — it must be set by
pam/systemd. Missing required variables return LinuxEnvironmentMissing.
Flatpak and Snap runtimes remap $XDG_* variables to sandbox paths
automatically; no special handling is needed in the plugin.
Check out the examples/tauri-app directory for a working example of how to use this plugin.
To run the example app:
# Desktop
npm run example:dev
# Windows MSIX (package identity via winapp CLI)
npm run example:dev:msix
# iOS
npm run example:init:ios # first time only
npm run example:dev:ios
# Android
npm run example:init:android # first time only
npm run example:dev:androidThe example app includes a Package.appxmanifest and a dev:msix script
that uses the winapp CLI to build the app and launch it with
package identity (winapp run). From the repo root:
npm run example:dev:msixThis runs examples/tauri-app/scripts/run-msix.ps1, which debug-builds the
example, then registers and launches it as a loose-layout MSIX package. In the
app UI, switch the WinMsix radio button to exercise
WindowsApplicationDataPath variants; Win32 covers known-folder paths for
unpackaged runs (npm run example:dev).
Prerequisites: Windows 11, winapp CLI
(winget install microsoft.winappcli --source winget), and PowerShell.
Before running on iOS, you must set your Apple Development Team ID in
examples/tauri-app/src-tauri/gen/apple/tauri-app.xcodeproj/project.pbxproj.
The best way to do this is to open this file in Xcode and set the Team in Signing & Capabilities.
When deploying to a physical iOS device, you may also need to trust the developer certificate on the device: go to Settings > General > VPN & Device Management, select your developer profile, and tap Trust.
This project follows the Silvermine standardization guidelines. Key standards include:
- EditorConfig: Consistent editor settings across the team
- Markdownlint: Markdown linting for documentation
- Commitlint: Conventional commit message format
- Code Style: 3-space indentation, LF line endings
npm run standardsMIT
Contributions are welcome! Please follow the established coding standards and commit message conventions.