#file-dialog #android #tauri #fs

sys tauri-plugin-android-fs

Android file system API for Tauri

43 stable releases (7 major)

new 8.3.2 Apr 18, 2025
7.0.2 Mar 31, 2025
6.0.0 Mar 30, 2025
5.0.1 Mar 29, 2025
1.6.0 Feb 21, 2025

#167 in GUI

Download history 1007/week @ 2025-02-12 740/week @ 2025-02-19 595/week @ 2025-02-26 710/week @ 2025-03-05 383/week @ 2025-03-12 165/week @ 2025-03-19 649/week @ 2025-03-26 270/week @ 2025-04-02 409/week @ 2025-04-09

1,541 downloads per month

MIT/Apache

140KB
2.5K SLoC

Kotlin 1.5K SLoC // 0.0% comments Rust 1K SLoC // 0.0% comments Prolog 18 SLoC

Note: I’m using a translation tool, so there may be some inappropriate expressions.

Overview

The Android file system is strict and complex. This plugin was created to provide practical file operations. You don’t need to set special permissions or configurations.

This crate aims to provide practicality through detailed and clear documentation and a wide range of options, rather than focusing on clean functions that require minimal attention. If an error or unexpected behavior occurs, please first check the documentation for the function and functions that provided arguments. If the issue persists, it would be helpful if you could let us know on Github. Function requests are also welcome.

Setup

Register this plugin with your Tauri project:

src-tauri/src/lib.rs

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_android_fs::init()) // This
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The current dialog in this plugin has an issue. To avoid this, please follow these two steps:

src-tauri/Cargo.toml

[dependencies]
tauri-plugin-android-fs = { features = ["avoid-issue1"], .. }

src-tauri/capabilities/default.json

{
  ..
  "permissions": [
    "android-fs:default",
    ..
  ]
}

Usage

Currently, this plugin only provides a Rust-side API, not a NPM library.

Then, there are three main ways to manipulate files:

1. Dialog

Opens the file/folder picker to read and write user-selected entries.

use tauri_plugin_android_fs::{AndroidFs, AndroidFsExt, FileAccessMode};

fn file_picker_example(app: tauri::AppHandle) -> tauri_plugin_android_fs::Result<()> {
    let api = app.android_fs();
    
    // pick files to read and write
    let selected_files = api.show_open_file_dialog(
        None, // Initial location
        &["*/*"], // Target MIME types
        true, // Allow multiple files
    )?;

    if selected_files.is_empty() {
        // Handle cancel
    }
    else {
        for uri in selected_files {
            let file_type = api.get_mime_type(&uri)?.unwrap(); // If file, this returns no None.
            let file_name = api.get_name(&uri)?;

            {
                // Handle readonly file.
                let file: std::fs::File = api.open_file(&uri, FileAccessMode::Read)?;
            }

            {
                // Handle writeonly file. 
                // This truncate existing contents.
                let file: std::fs::File = api.open_file(&uri, FileAccessMode::WriteTruncate)?;
            }

            // If you need to use file data on frontend, 
            // consider using Tauri’s custom protocols for efficient transmission.
            // Or convert to FilePath and use tauri_plugin_fs functions such as 'read' on frontend.
            let file_path: tauri_plugin_fs::FilePath = uri.into();
        }
    }
    Ok(())
}
use tauri_plugin_android_fs::{AndroidFs, AndroidFsExt, Entry};

fn dir_picker_example(app: tauri::AppHandle) -> tauri_plugin_android_fs::Result<()> {
    let api = app.android_fs();

    // pick folder to read and write
    let selected_folder = api.show_manage_dir_dialog(
        None, // Initial location
    )?;

    if let Some(dir_uri) = selected_folder {
        for entry in api.read_dir(&dir_uri)? {
            match entry {
                Entry::File { name, uri, last_modified, len, mime_type, .. } => {
                    // handle file
                },
                Entry::Dir { name, uri, last_modified, .. } => {
                    // handle folder
                },
            }
        }
    } 
    else {
        // Handle cancel
    }
    
    Ok(())
}
use tauri_plugin_android_fs::{AndroidFs, AndroidFsExt, FileUri, PersistableAccessMode, PrivateDir, PrivateStorage};

/// Opens a dialog to save the file and write contents to the selected file.
/// 
/// return Ok(false) when canceled by user.  
/// return Ok(true) when success.
fn save_file_with_dialog(
    app: tauri::AppHandle,
    file_name: &str,
    mime_type: &str,
    contents: &[u8],
) -> tauri_plugin_android_fs::Result<bool> {

    let api = app.android_fs();

    // pick file to write
    let file_uri = api.show_save_file_dialog(
        None, // Initial location
        file_name, // Initial file name
        Some(mime_type), // MIME type
    )?;

    let Some(file_uri) = file_uri else {
        return Ok(false)
    };

    // write contents
    if let Err(e) = api.write(&file_uri, contents) {
        // handle err
        let _ = api.remove_file(&file_uri);
        return Err(e)
    }
    
    Ok(true)
}

/// Open a dialog to select a directory, 
/// and create a new file at the relative_path position from it,
/// then write contents to it.  
/// If a folder has been selected in the past, use it without opening a dialog.
/// 
/// return Ok(false) when canceled by user.  
/// return Ok(true) when success.  
fn save_file_with_dir_dialog(
    app: tauri::AppHandle, 
    relative_path: &str,
    mime_type: &str,
    contents: &[u8],
) -> tauri_plugin_android_fs::Result<bool> {

    const DEST_DIR_URI_DATA_RELATIVE_PATH: &str = "01JQMFWVH65YNCWM31V3DZG6GR";
    let api = app.android_fs();

    // Retrieve previously retrieved dest dir uri, if exists.
    let dest_dir_uri = api
        .private_storage()
        .read_to_string(PrivateDir::Data, DEST_DIR_URI_DATA_RELATIVE_PATH)
        .and_then(|u| FileUri::from_str(&u))
        .ok();

    // Check permission, if exists.
    let dest_dir_uri = match dest_dir_uri {
        Some(dest_dir_uri) => {
            if api.check_persisted_uri_permission(&dest_dir_uri, PersistableAccessMode::ReadAndWrite)? {
                Some(dest_dir_uri)
            }
            else {
                None
            }
        },
        None => None
    };
    
    // If there is no valid dest dir, select a new one
    let dest_dir_uri = match dest_dir_uri {
        Some(dest_dir_uri) => dest_dir_uri,
        None => {
            // Show folder picker
            let Some(uri) = api.show_manage_dir_dialog(None)? else {
                // Canceled by user
                return Ok(false)
            };

            // Store uri
            api.private_storage().write(
                PrivateDir::Data, 
                DEST_DIR_URI_DATA_RELATIVE_PATH, 
                uri.to_string()?.as_bytes()
            )?;

            // Persist uri permission across app restarts
            api.take_persistable_uri_permission(&uri)?;

            uri
        },
    };
    
    // create a new empty file in dest folder
    let new_file_uri = api.create_file(
        &dest_dir_uri, 
        relative_path, 
        Some(mime_type)
    )?;

    // write contents
    if let Err(e) = api.write(&new_file_uri, contents) {
        // handle err
        let _ = api.remove_file(&new_file_uri);
        return Err(e)
    }
    
    Ok(true)
}

2. Public Storage

File storage that is available to other applications and users. Currently, this is for Android 10 (API level 29) or higher.

use tauri_plugin_android_fs::{AndroidFs, AndroidFsExt, PublicGeneralPurposeDir, PublicImageDir, PublicStorage};

fn example(app: tauri::AppHandle) -> tauri_plugin_android_fs::Result<()> {
    let api = app.android_fs();
    let storage = api.public_storage();
    let contents: Vec<u8> = vec![];

    // create a new empty PNG image file
    //
    // This path is represented as follows:
    //   ~/Pictures/{app_name}/my-image.png
    //   $HOME/Pictures/{app_name}/my-image.png
    //   /storage/emulated/0/Pictures/{app_name}/my-image.png
    let uri = storage.create_file_in_public_app_dir(
         PublicImageDir::Pictures, // Base directory
         "my-image.png", // Relative file path
         Some("image/png") // Mime type
    )?;

    // write the contents to the PNG image
    if let Err(e) = api.write(&uri, &contents) {
        // handle err
        let _ = api.remove_file(&uri);
        return Err(e)
    }


    // create a new empty text file
    //
    // This path is represented as follows:
    //   ~/Documents/{app_name}/2025-3-2/data.txt
    //   $HOME/Documents/{app_name}/2025-3-2/data.txt
    //   /storage/emulated/0/Documents/{app_name}/2025-3-2/data.txt
    let uri = storage.create_file_in_public_app_dir(
         PublicGeneralPurposeDir::Documents, // Base directory
         "2025-3-2/data.txt", // Relative file path
         Some("text/plain") // Mime type
    )?;

    // write the contents to the text file
    if let Err(e) = api.write(&uri, &contents) {
        // handle err
        let _ = api.remove_file(&uri);
        return Err(e)
    }

    Ok(())
}

3. Private Storage

File storage intended for the app’s use only.

use tauri_plugin_android_fs::{AndroidFs, AndroidFsExt, PrivateDir, PrivateStorage};

fn example(app: tauri::AppHandle) -> tauri_plugin_android_fs::Result<()> {
    let storage = app.android_fs().private_storage();

    // Get the absolute path.
    // Apps can fully manage entries within this directory.
    let _cache_dir_path: std::path::PathBuf = storage.resolve_path(PrivateDir::Cache)?;
    let _data_dir_path: std::path::PathBuf = storage.resolve_path(PrivateDir::Data)?;


    // Write the contents.
    // This is wrapper of above resolve_path and std::fs
    storage.write(
        PrivateDir::Data, // Base directory
        "config/data1", // Relative file path
        &[]
    )?;

    // Read the contents.
    // This is wrapper of above resolve_path and std::fs
    let contents = storage.read(
        PrivateDir::Data, // Base directory
        "config/data1" // Relative file path
    )?;

    Ok(())
}

Link

License

MIT OR Apache-2.0

Dependencies

~19–54MB
~869K SLoC