Skip to content

silvermine/tauri-plugin-fs-resolver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tauri Plugin FS Resolver

CI

Platform-specific file system paths for Tauri 2.x apps.

Features

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²

Getting Started

Installation

  1. Install NPM dependencies:

    npm install
  2. Build the TypeScript bindings:

    npm run build
  3. Build the Rust plugin:

    cargo build

Tests

Run Rust tests:

cargo test

Run Typescript tests:

npm run test

Install

This plugin requires a Rust version of at least 1.94.0

Rust

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" }

JavaScript/TypeScript

Install the JavaScript bindings:

npm install @silvermine/tauri-plugin-fs-resolver

Usage

Prerequisites

Initialize 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");
}

API

The plugin exposes two levels of API:

  1. PathMapping — a cross-platform path definition that maps each platform to its correct path enum variant. Define the mapping once, then call resolve() 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.

  2. 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).

Architecture

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 calling invoke() 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.

PathMapping

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)
}

Platform-specific resolve functions

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))?;

Implementation

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 paths

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 win32 path while running inside an MSIX package returns Win32PathInvokedFromMsixPackagedContext.
  • Resolving a winMsix path outside a package returns WindowsApplicationDataPathInvokedFromWin32Context.

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 paths

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.

Examples

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:android

Windows MSIX testing

The 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:msix

This 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.

iOS Setup

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.

Development Standards

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

Running Standards Checks

npm run standards

License

MIT

Contributing

Contributions are welcome! Please follow the established coding standards and commit message conventions.

About

File system path resolution for use in Tauri v2 apps

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors