feat(fs): resolve content URIs on Android (#1658)

* Implemented writeTextFile on Android.

* Added license headers.

* fix fmt checks.

* implement more file APIs

* change file

* cleanup

* refactor dialog plugin to leverage new FS APIs

* implement metadata functions

* fix build

* expose FS rust API

* resolve resources on android

* update pnpm

* update docs

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
mikoto2000
2024-08-23 13:35:53 -03:00
committed by GitHub
co-authored by Lucas Nogueira
parent 3c52f30ea4
commit 448846b834
38 changed files with 1951 additions and 820 deletions
+190 -102
View File
@@ -17,9 +17,6 @@ use tauri::{
Manager, Runtime,
};
#[cfg(any(desktop, target_os = "ios"))]
use std::fs;
use std::{
path::{Path, PathBuf},
sync::mpsc::sync_channel,
@@ -66,6 +63,87 @@ impl<R: Runtime, T: Manager<R>> crate::DialogExt<R> for T {
}
impl<R: Runtime> Dialog<R> {
/// Create a new messaging dialog builder.
/// The dialog can optionally ask the user for confirmation or include an OK button.
///
/// # Examples
///
/// - Message dialog:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app
/// .dialog()
/// .message("Tauri is Awesome!")
/// .show(|_| {
/// println!("dialog closed");
/// });
/// Ok(())
/// });
/// ```
///
/// - Ask dialog:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app.dialog()
/// .message("Are you sure?")
/// .ok_button_label("Yes")
/// .cancel_button_label("No")
/// .show(|yes| {
/// println!("user said {}", if yes { "yes" } else { "no" });
/// });
/// Ok(())
/// });
/// ```
///
/// - Message dialog with OK button:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app.dialog()
/// .message("Job completed successfully")
/// .ok_button_label("Ok")
/// .show(|_| {
/// println!("dialog closed");
/// });
/// Ok(())
/// });
/// ```
///
/// # `show` vs `blocking_show`
///
/// The dialog builder includes two separate APIs for rendering the dialog: `show` and `blocking_show`.
/// The `show` function is asynchronous and takes a closure to be executed when the dialog is closed.
/// To block the current thread until the user acted on the dialog, you can use `blocking_show`,
/// but note that it cannot be executed on the main thread as it will freeze your application.
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// let handle = app.handle().clone();
/// std::thread::spawn(move || {
/// let yes = handle.dialog()
/// .message("Are you sure?")
/// .ok_button_label("Yes")
/// .cancel_button_label("No")
/// .blocking_show();
/// });
///
/// Ok(())
/// });
/// ```
pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> {
MessageDialogBuilder::new(
self.clone(),
@@ -74,6 +152,7 @@ impl<R: Runtime> Dialog<R> {
)
}
/// Creates a new builder for dialogs that lets ths user select file(s) or folder(s).
pub fn file(&self) -> FileDialogBuilder<R> {
FileDialogBuilder::new(self.clone())
}
@@ -216,36 +295,52 @@ impl<R: Runtime> MessageDialogBuilder<R> {
}
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct FileResponse {
pub base64_data: Option<String>,
pub duration: Option<u64>,
pub height: Option<usize>,
pub width: Option<usize>,
pub mime_type: Option<String>,
pub modified_at: Option<u64>,
pub name: Option<String>,
pub path: PathBuf,
pub size: u64,
/// Represents either a filesystem path or a URI pointing to a file
/// such as `file://` URIs or Android `content://` URIs.
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum FilePath {
Url(url::Url),
Path(PathBuf),
}
impl FileResponse {
#[cfg(desktop)]
fn new(path: PathBuf) -> Self {
let metadata = fs::metadata(&path);
let metadata = metadata.as_ref();
Self {
base64_data: None,
duration: None,
height: None,
width: None,
mime_type: None,
modified_at: metadata.ok().and_then(|m| to_msec(m.modified())),
name: path.file_name().map(|f| f.to_string_lossy().into_owned()),
path,
size: metadata.map(|m| m.len()).unwrap_or(0),
impl From<PathBuf> for FilePath {
fn from(value: PathBuf) -> Self {
Self::Path(value)
}
}
impl From<url::Url> for FilePath {
fn from(value: url::Url) -> Self {
Self::Url(value)
}
}
impl From<FilePath> for tauri_plugin_fs::FilePath {
fn from(value: FilePath) -> Self {
match value {
FilePath::Path(p) => tauri_plugin_fs::FilePath::Path(p),
FilePath::Url(url) => tauri_plugin_fs::FilePath::Url(url),
}
}
}
impl FilePath {
fn simplified(self) -> Self {
match self {
Self::Url(url) => Self::Url(url),
Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()),
}
}
#[inline]
fn path(&self) -> Result<PathBuf> {
match self {
Self::Url(url) => url
.to_file_path()
.map(PathBuf::from)
.map_err(|_| Error::InvalidPathUrl),
Self::Path(p) => Ok(p.to_owned()),
}
}
}
@@ -363,21 +458,18 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().pick_file(|file_path| {
/// // do something with the optional file path here
/// // the file path is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
pub fn pick_file<F: FnOnce(Option<FileResponse>) + Send + 'static>(self, f: F) {
#[cfg(desktop)]
let f = |path: Option<PathBuf>| f(path.map(FileResponse::new));
pub fn pick_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_file(self, f)
}
@@ -385,29 +477,44 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// This is not a blocking operation,
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
///
/// # Reading the files
///
/// The file paths cannot be read directly on Android as they are behind a content URI.
/// The recommended way to read the files is using the [`fs`](https://v2.tauri.app/plugin/file-system/) plugin:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// use tauri_plugin_fs::FsExt;
/// tauri::Builder::default()
/// .setup(|app| {
/// let handle = app.handle().clone();
/// app.dialog().file().pick_file(move |file_path| {
/// let Some(path) = file_path else { return };
/// let Ok(contents) = handle.fs().read_to_string(path) else {
/// eprintln!("failed to read file, <todo add error handling!>");
/// return;
/// };
/// });
/// Ok(())
/// });
/// ```
///
/// See <https://developer.android.com/guide/topics/providers/content-provider-basics> for more information.
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().pick_files(|file_paths| {
/// // do something with the optional file paths here
/// // the file paths value is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
pub fn pick_files<F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(self, f: F) {
#[cfg(desktop)]
let f = |paths: Option<Vec<PathBuf>>| {
f(paths.map(|p| {
p.into_iter()
.map(FileResponse::new)
.collect::<Vec<FileResponse>>()
}))
};
pub fn pick_files<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
pick_files(self, f)
}
@@ -417,20 +524,19 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().pick_folder(|folder_path| {
/// // do something with the optional folder path here
/// // the folder path is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
#[cfg(desktop)]
pub fn pick_folder<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
pub fn pick_folder<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_folder(self, f)
}
@@ -440,20 +546,19 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().pick_folders(|file_paths| {
/// // do something with the optional folder paths here
/// // the folder paths value is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
#[cfg(desktop)]
pub fn pick_folders<F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(self, f: F) {
pub fn pick_folders<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
pick_folders(self, f)
}
@@ -464,19 +569,18 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().save_file(|file_path| {
/// // do something with the optional file path here
/// // the file path is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
pub fn save_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
save_file(self, f)
}
}
@@ -489,7 +593,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -498,7 +602,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
pub fn blocking_pick_file(self) -> Option<FileResponse> {
pub fn blocking_pick_file(self) -> Option<FilePath> {
blocking_fn!(self, pick_file)
}
@@ -508,7 +612,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -517,7 +621,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file paths value is `None` if the user closed the dialog
/// }
/// ```
pub fn blocking_pick_files(self) -> Option<Vec<FileResponse>> {
pub fn blocking_pick_files(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_files)
}
@@ -527,7 +631,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -537,7 +641,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// }
/// ```
#[cfg(desktop)]
pub fn blocking_pick_folder(self) -> Option<PathBuf> {
pub fn blocking_pick_folder(self) -> Option<FilePath> {
blocking_fn!(self, pick_folder)
}
@@ -547,7 +651,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -557,7 +661,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// }
/// ```
#[cfg(desktop)]
pub fn blocking_pick_folders(self) -> Option<Vec<PathBuf>> {
pub fn blocking_pick_folders(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_folders)
}
@@ -567,7 +671,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -576,23 +680,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
pub fn blocking_save_file(self) -> Option<PathBuf> {
pub fn blocking_save_file(self) -> Option<FilePath> {
blocking_fn!(self, save_file)
}
}
// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913
#[cfg(desktop)]
#[inline]
fn to_msec(maybe_time: std::result::Result<std::time::SystemTime, std::io::Error>) -> Option<u64> {
match maybe_time {
Ok(time) => {
let msec = time
.duration_since(std::time::UNIX_EPOCH)
.map(|t| t.as_millis() as u64)
.unwrap_or_else(|err| err.duration().as_millis() as u64);
Some(msec)
}
Err(_) => None,
}
}