mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-08-19 19:17:15 +02:00
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:
co-authored by
Lucas Nogueira
parent
3c52f30ea4
commit
448846b834
+190
-102
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user