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-24 01:35:53 +09:00
committed by GitHub
parent 3c52f30ea4
commit 448846b834
38 changed files with 1951 additions and 820 deletions
+335 -164
View File
@@ -9,18 +9,60 @@ use tauri::{
ipc::{CommandScope, GlobalScope},
path::{BaseDirectory, SafePathBuf},
utils::config::FsScope,
Manager, Resource, ResourceId, Runtime, Webview,
AppHandle, Manager, Resource, ResourceId, Runtime, Webview,
};
use std::{
fs::File,
io::{BufReader, Lines, Read, Write},
path::{Path, PathBuf},
str::FromStr,
sync::Mutex,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{scope::Entry, Error, FsExt};
use crate::{scope::Entry, Error, FilePath, FsExt};
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum SafeFilePath {
Url(url::Url),
Path(SafePathBuf),
}
impl From<SafeFilePath> for FilePath {
fn from(value: SafeFilePath) -> Self {
match value {
SafeFilePath::Url(url) => FilePath::Url(url),
SafeFilePath::Path(p) => FilePath::Path(p.as_ref().to_owned()),
}
}
}
impl FromStr for SafeFilePath {
type Err = CommandError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(url) = url::Url::from_str(s) {
Ok(Self::Url(url))
} else {
Ok(Self::Path(SafePathBuf::new(s.into())?))
}
}
}
impl SafeFilePath {
#[inline]
fn into_path(self) -> CommandResult<SafePathBuf> {
match self {
Self::Url(url) => SafePathBuf::new(
url.to_file_path()
.map_err(|_| format!("failed to get path from {url}"))?,
)
.map_err(Into::into),
Self::Path(p) => Ok(p),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CommandError {
@@ -31,6 +73,8 @@ pub enum CommandError {
#[error(transparent)]
Tauri(#[from] tauri::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
UrlParseError(#[from] url::ParseError),
#[cfg(feature = "watch")]
#[error(transparent)]
@@ -64,7 +108,7 @@ impl Serialize for CommandError {
pub type CommandResult<T> = std::result::Result<T, CommandError>;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BaseOptions {
base_dir: Option<BaseDirectory>,
@@ -75,7 +119,7 @@ pub fn create<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<ResourceId> {
let resolved_path = resolve_path(
@@ -95,29 +139,13 @@ pub fn create<R: Runtime>(
Ok(rid)
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenOptions {
#[serde(flatten)]
base: BaseOptions,
#[serde(default = "default_true")]
read: bool,
#[serde(default)]
write: bool,
#[serde(default)]
append: bool,
#[serde(default)]
truncate: bool,
#[serde(default)]
create: bool,
#[serde(default)]
create_new: bool,
#[allow(unused)]
mode: Option<u32>,
}
fn default_true() -> bool {
true
#[serde(flatten)]
options: crate::OpenOptions,
}
#[tauri::command]
@@ -125,44 +153,35 @@ pub fn open<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<OpenOptions>,
) -> CommandResult<ResourceId> {
let resolved_path = resolve_path(
let (file, _path) = resolve_file(
&webview,
&global_scope,
&command_scope,
path,
options.as_ref().and_then(|o| o.base.base_dir),
)?;
let mut opts = std::fs::OpenOptions::new();
// default to read-only
opts.read(true);
if let Some(options) = options {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
if let Some(mode) = options.mode {
opts.mode(mode);
if let Some(opts) = options {
OpenOptions {
base: opts.base,
options: opts.options,
}
}
opts.read(options.read)
.create(options.create)
.write(options.write)
.truncate(options.truncate)
.append(options.append)
.create_new(options.create_new);
}
let file = opts.open(&resolved_path).map_err(|e| {
format!(
"failed to open file at path: {} with error: {e}",
resolved_path.display()
)
})?;
} else {
OpenOptions {
base: BaseOptions { base_dir: None },
options: crate::OpenOptions {
read: true,
write: false,
truncate: false,
create: false,
create_new: false,
append: false,
mode: None,
custom_flags: None,
},
}
},
)?;
let rid = webview.resources_table().add(StdFileResource::new(file));
@@ -186,8 +205,8 @@ pub async fn copy_file<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
from_path: SafePathBuf,
to_path: SafePathBuf,
from_path: SafeFilePath,
to_path: SafeFilePath,
options: Option<CopyFileOptions>,
) -> CommandResult<()> {
let resolved_from_path = resolve_path(
@@ -228,7 +247,7 @@ pub fn mkdir<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<MkdirOptions>,
) -> CommandResult<()> {
let resolved_path = resolve_path(
@@ -295,7 +314,7 @@ pub async fn read_dir<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<Vec<DirEntry>> {
let resolved_path = resolve_path(
@@ -334,25 +353,35 @@ pub async fn read_file<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<tauri::ipc::Response> {
let resolved_path = resolve_path(
let (mut file, path) = resolve_file(
&webview,
&global_scope,
&command_scope,
path,
options.as_ref().and_then(|o| o.base_dir),
OpenOptions {
base: BaseOptions {
base_dir: options.as_ref().and_then(|o| o.base_dir),
},
options: crate::OpenOptions {
read: true,
..Default::default()
},
},
)?;
std::fs::read(&resolved_path)
.map(tauri::ipc::Response::new)
.map_err(|e| {
format!(
"failed to read file at path: {} with error: {e}",
resolved_path.display()
)
})
.map_err(Into::into)
let mut contents = Vec::new();
file.read_to_end(&mut contents).map_err(|e| {
format!(
"failed to read file as text at path: {} with error: {e}",
path.display()
)
})?;
Ok(tauri::ipc::Response::new(contents))
}
#[tauri::command]
@@ -360,24 +389,35 @@ pub async fn read_text_file<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<String> {
let resolved_path = resolve_path(
let (mut file, path) = resolve_file(
&webview,
&global_scope,
&command_scope,
path,
options.as_ref().and_then(|o| o.base_dir),
OpenOptions {
base: BaseOptions {
base_dir: options.as_ref().and_then(|o| o.base_dir),
},
options: crate::OpenOptions {
read: true,
..Default::default()
},
},
)?;
std::fs::read_to_string(&resolved_path)
.map_err(|e| {
format!(
"failed to read file as text at path: {} with error: {e}",
resolved_path.display()
)
})
.map_err(Into::into)
let mut contents = String::new();
file.read_to_string(&mut contents).map_err(|e| {
format!(
"failed to read file as text at path: {} with error: {e}",
path.display()
)
})?;
Ok(contents)
}
#[tauri::command]
@@ -385,7 +425,7 @@ pub fn read_text_file_lines<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<ResourceId> {
use std::io::BufRead;
@@ -441,7 +481,7 @@ pub fn remove<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<RemoveOptions>,
) -> CommandResult<()> {
let resolved_path = resolve_path(
@@ -509,8 +549,8 @@ pub fn rename<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
old_path: SafePathBuf,
new_path: SafePathBuf,
old_path: SafeFilePath,
new_path: SafeFilePath,
options: Option<RenameOptions>,
) -> CommandResult<()> {
let resolved_old_path = resolve_path(
@@ -566,27 +606,109 @@ pub async fn seek<R: Runtime>(
.map_err(Into::into)
}
#[tauri::command]
pub fn stat<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
#[cfg(target_os = "android")]
fn get_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Metadata>>(
metadata_fn: F,
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<FileInfo> {
) -> CommandResult<std::fs::Metadata> {
match path {
SafeFilePath::Url(url) => {
let (file, path) = resolve_file(
webview,
global_scope,
command_scope,
SafeFilePath::Url(url),
OpenOptions {
base: BaseOptions { base_dir: None },
options: crate::OpenOptions {
read: true,
..Default::default()
},
},
)?;
file.metadata().map_err(|e| {
format!(
"failed to get metadata of path: {} with error: {e}",
path.display()
)
.into()
})
}
SafeFilePath::Path(p) => get_fs_metadata(
metadata_fn,
webview,
global_scope,
command_scope,
SafeFilePath::Path(p),
options,
),
}
}
#[cfg(not(target_os = "android"))]
fn get_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Metadata>>(
metadata_fn: F,
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<std::fs::Metadata> {
get_fs_metadata(
metadata_fn,
webview,
global_scope,
command_scope,
path,
options,
)
}
fn get_fs_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Metadata>>(
metadata_fn: F,
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<std::fs::Metadata> {
let resolved_path = resolve_path(
&webview,
&global_scope,
&command_scope,
webview,
global_scope,
command_scope,
path,
options.as_ref().and_then(|o| o.base_dir),
)?;
let metadata = std::fs::metadata(&resolved_path).map_err(|e| {
let metadata = metadata_fn(&resolved_path).map_err(|e| {
format!(
"failed to get metadata of path: {} with error: {e}",
resolved_path.display()
)
})?;
Ok(metadata)
}
#[tauri::command]
pub fn stat<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<FileInfo> {
let metadata = get_metadata(
|p| std::fs::metadata(p),
&webview,
&global_scope,
&command_scope,
path,
options,
)?;
Ok(get_stat(metadata))
}
@@ -595,22 +717,17 @@ pub fn lstat<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<FileInfo> {
let resolved_path = resolve_path(
let metadata = get_metadata(
|p| std::fs::symlink_metadata(p),
&webview,
&global_scope,
&command_scope,
path,
options.as_ref().and_then(|o| o.base_dir),
options,
)?;
let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| {
format!(
"failed to get metadata of path: {} with error: {e}",
resolved_path.display()
)
})?;
Ok(get_stat(metadata))
}
@@ -627,7 +744,7 @@ pub async fn truncate<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
len: Option<u64>,
options: Option<BaseOptions>,
) -> CommandResult<()> {
@@ -704,49 +821,51 @@ fn write_file_inner<R: Runtime>(
webview: Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
data: &[u8],
options: Option<WriteFileOptions>,
) -> CommandResult<()> {
let resolved_path = resolve_path(
let (mut file, path) = resolve_file(
&webview,
global_scope,
command_scope,
path,
options.as_ref().and_then(|o| o.base.base_dir),
)?;
let mut opts = std::fs::OpenOptions::new();
// defaults
opts.read(false).write(true).truncate(true).create(true);
if let Some(options) = options {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
if let Some(mode) = options.mode {
opts.mode(mode);
if let Some(opts) = options {
OpenOptions {
base: opts.base,
options: crate::OpenOptions {
read: false,
write: true,
create: opts.create,
truncate: !opts.append,
append: opts.append,
create_new: opts.create_new,
mode: opts.mode,
custom_flags: None,
},
}
}
opts.create(options.create)
.append(options.append)
.truncate(!options.append)
.create_new(options.create_new);
}
let mut file = opts.open(&resolved_path).map_err(|e| {
format!(
"failed to open file at path: {} with error: {e}",
resolved_path.display()
)
})?;
} else {
OpenOptions {
base: BaseOptions { base_dir: None },
options: crate::OpenOptions {
read: false,
write: true,
truncate: true,
create: true,
create_new: false,
append: false,
mode: None,
custom_flags: None,
},
}
},
)?;
file.write_all(data)
.map_err(|e| {
format!(
"failed to write bytes to file at path: {} with error: {e}",
resolved_path.display()
path.display()
)
})
.map_err(Into::into)
@@ -768,7 +887,7 @@ pub async fn write_file<R: Runtime>(
p.to_str()
.map_err(|e| anyhow::anyhow!("invalid path: {e}").into())
})
.and_then(|p| SafePathBuf::new(p.into()).map_err(CommandError::from))?;
.and_then(|p| SafeFilePath::from_str(p).map_err(CommandError::from))?;
let options = request
.headers()
.get("options")
@@ -782,12 +901,13 @@ pub async fn write_file<R: Runtime>(
#[tauri::command]
pub async fn write_text_file<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
#[allow(unused)] app: AppHandle<R>,
#[allow(unused)] webview: Webview<R>,
#[allow(unused)] global_scope: GlobalScope<Entry>,
#[allow(unused)] command_scope: CommandScope<Entry>,
path: SafeFilePath,
data: String,
options: Option<WriteFileOptions>,
#[allow(unused)] options: Option<WriteFileOptions>,
) -> CommandResult<()> {
write_file_inner(
webview,
@@ -804,7 +924,7 @@ pub fn exists<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<bool> {
let resolved_path = resolve_path(
@@ -817,24 +937,87 @@ pub fn exists<R: Runtime>(
Ok(resolved_path.exists())
}
pub fn resolve_path<R: Runtime>(
app: &Webview<R>,
#[cfg(not(target_os = "android"))]
pub fn resolve_file<R: Runtime>(
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafePathBuf,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
resolve_file_in_fs(webview, global_scope, command_scope, path, open_options)
}
fn resolve_file_in_fs<R: Runtime>(
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
let path = resolve_path(
webview,
global_scope,
command_scope,
path,
open_options.base.base_dir,
)?;
let file = std::fs::OpenOptions::from(open_options.options)
.open(&path)
.map_err(|e| {
format!(
"failed to open file at path: {} with error: {e}",
path.display()
)
})?;
Ok((file, path))
}
#[cfg(target_os = "android")]
pub fn resolve_file<R: Runtime>(
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
match path {
SafeFilePath::Url(url) => {
let path = url.as_str().into();
let file = webview
.fs()
.open(SafeFilePath::Url(url), open_options.options)?;
Ok((file, path))
}
SafeFilePath::Path(path) => resolve_file_in_fs(
webview,
global_scope,
command_scope,
SafeFilePath::Path(path),
open_options,
),
}
}
pub fn resolve_path<R: Runtime>(
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
base_dir: Option<BaseDirectory>,
) -> CommandResult<PathBuf> {
let path = file_url_to_safe_pathbuf(path)?;
let path = path.into_path()?;
let path = if let Some(base_dir) = base_dir {
app.path().resolve(&path, base_dir)?
webview.path().resolve(&path, base_dir)?
} else {
path.as_ref().to_path_buf()
};
let scope = tauri::scope::fs::Scope::new(
app,
webview,
&FsScope::Scope {
allow: app
allow: webview
.fs_scope()
.allowed
.lock()
@@ -844,7 +1027,7 @@ pub fn resolve_path<R: Runtime>(
.chain(global_scope.allows().iter().map(|e| e.path.clone()))
.chain(command_scope.allows().iter().map(|e| e.path.clone()))
.collect(),
deny: app
deny: webview
.fs_scope()
.denied
.lock()
@@ -854,7 +1037,7 @@ pub fn resolve_path<R: Runtime>(
.chain(global_scope.denies().iter().map(|e| e.path.clone()))
.chain(command_scope.denies().iter().map(|e| e.path.clone()))
.collect(),
require_literal_leading_dot: app.fs_scope().require_literal_leading_dot,
require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot,
},
)?;
@@ -865,18 +1048,6 @@ pub fn resolve_path<R: Runtime>(
}
}
#[inline]
fn file_url_to_safe_pathbuf(path: SafePathBuf) -> CommandResult<SafePathBuf> {
if path.as_ref().starts_with("file:") {
let url = url::Url::parse(&path.display().to_string())?
.to_file_path()
.map_err(|_| "failed to get path from `file:` url")?;
SafePathBuf::new(url).map_err(Into::into)
} else {
Ok(path)
}
}
struct StdFileResource(Mutex<File>);
impl StdFileResource {
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::PathBuf;
use tauri::{AppHandle, Runtime};
use crate::{FilePath, OpenOptions};
pub struct Fs<R: Runtime>(pub(crate) AppHandle<R>);
fn path_or_err<P: Into<FilePath>>(p: P) -> std::io::Result<PathBuf> {
match p.into() {
FilePath::Path(p) => Ok(p),
FilePath::Url(u) if u.scheme() == "file" => u
.to_file_path()
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL")),
FilePath::Url(_) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot use a URL to load files on desktop and iOS",
)),
}
}
impl<R: Runtime> Fs<R> {
pub fn open<P: Into<FilePath>>(
&self,
path: P,
opts: OpenOptions,
) -> std::io::Result<std::fs::File> {
let path = path_or_err(path)?;
std::fs::OpenOptions::from(opts).open(path)
}
}
+3
View File
@@ -23,6 +23,9 @@ pub enum Error {
#[cfg(feature = "watch")]
#[error(transparent)]
Watch(#[from] notify::Error),
#[cfg(target_os = "android")]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
}
impl Serialize for Error {
+384
View File
@@ -11,6 +11,15 @@
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use std::{
convert::Infallible,
fmt,
io::Read,
path::{Path, PathBuf},
str::FromStr,
};
use serde::Deserialize;
use tauri::{
ipc::ScopeObject,
plugin::{Builder as PluginBuilder, TauriPlugin},
@@ -20,16 +29,375 @@ use tauri::{
mod commands;
mod config;
#[cfg(not(target_os = "android"))]
mod desktop;
mod error;
#[cfg(target_os = "android")]
mod mobile;
#[cfg(target_os = "android")]
mod models;
mod scope;
#[cfg(feature = "watch")]
mod watcher;
#[cfg(not(target_os = "android"))]
pub use desktop::Fs;
#[cfg(target_os = "android")]
pub use mobile::Fs;
pub use error::Error;
pub use scope::{Event as ScopeEvent, Scope};
type Result<T> = std::result::Result<T, Error>;
/// Represents either a filesystem path or a URI pointing to a file
/// such as `file://` URIs or Android `content://` URIs.
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum FilePath {
Url(url::Url),
Path(PathBuf),
}
impl FromStr for FilePath {
type Err = Infallible;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if let Ok(url) = url::Url::from_str(s) {
Ok(Self::Url(url))
} else {
Ok(Self::Path(PathBuf::from(s)))
}
}
}
impl From<PathBuf> for FilePath {
fn from(value: PathBuf) -> Self {
Self::Path(value)
}
}
impl From<&Path> for FilePath {
fn from(value: &Path) -> Self {
Self::Path(value.to_owned())
}
}
impl From<&PathBuf> for FilePath {
fn from(value: &PathBuf) -> Self {
Self::Path(value.to_owned())
}
}
impl From<url::Url> for FilePath {
fn from(value: url::Url) -> Self {
Self::Url(value)
}
}
impl fmt::Display for FilePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Url(u) => u.fmt(f),
Self::Path(p) => p.display().fmt(f),
}
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenOptions {
#[serde(default = "default_true")]
read: bool,
#[serde(default)]
write: bool,
#[serde(default)]
append: bool,
#[serde(default)]
truncate: bool,
#[serde(default)]
create: bool,
#[serde(default)]
create_new: bool,
#[serde(default)]
mode: Option<u32>,
#[serde(default)]
custom_flags: Option<i32>,
}
fn default_true() -> bool {
true
}
impl From<OpenOptions> for std::fs::OpenOptions {
fn from(open_options: OpenOptions) -> Self {
let mut opts = std::fs::OpenOptions::new();
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
if let Some(mode) = open_options.mode {
opts.mode(mode);
}
if let Some(flags) = open_options.custom_flags {
opts.custom_flags(flags);
}
}
opts.read(open_options.read)
.write(open_options.write)
.create(open_options.create)
.append(open_options.append)
.truncate(open_options.truncate)
.create_new(open_options.create_new);
opts
}
}
impl OpenOptions {
/// Creates a blank new set of options ready for configuration.
///
/// All options are initially set to `false`.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let mut options = OpenOptions::new();
/// let file = options.read(true).open("foo.txt");
/// ```
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Sets the option for read access.
///
/// This option, when true, will indicate that the file should be
/// `read`-able if opened.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().read(true).open("foo.txt");
/// ```
pub fn read(&mut self, read: bool) -> &mut Self {
self.read = read;
self
}
/// Sets the option for write access.
///
/// This option, when true, will indicate that the file should be
/// `write`-able if opened.
///
/// If the file already exists, any write calls on it will overwrite its
/// contents, without truncating it.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().write(true).open("foo.txt");
/// ```
pub fn write(&mut self, write: bool) -> &mut Self {
self.write = write;
self
}
/// Sets the option for the append mode.
///
/// This option, when true, means that writes will append to a file instead
/// of overwriting previous contents.
/// Note that setting `.write(true).append(true)` has the same effect as
/// setting only `.append(true)`.
///
/// Append mode guarantees that writes will be positioned at the current end of file,
/// even when there are other processes or threads appending to the same file. This is
/// unlike <code>[seek]\([SeekFrom]::[End]\(0))</code> followed by `write()`, which
/// has a race between seeking and writing during which another writer can write, with
/// our `write()` overwriting their data.
///
/// Keep in mind that this does not necessarily guarantee that data appended by
/// different processes or threads does not interleave. The amount of data accepted a
/// single `write()` call depends on the operating system and file system. A
/// successful `write()` is allowed to write only part of the given data, so even if
/// you're careful to provide the whole message in a single call to `write()`, there
/// is no guarantee that it will be written out in full. If you rely on the filesystem
/// accepting the message in a single write, make sure that all data that belongs
/// together is written in one operation. This can be done by concatenating strings
/// before passing them to [`write()`].
///
/// If a file is opened with both read and append access, beware that after
/// opening, and after every write, the position for reading may be set at the
/// end of the file. So, before writing, save the current position (using
/// <code>[Seek]::[stream_position]</code>), and restore it before the next read.
///
/// ## Note
///
/// This function doesn't create the file if it doesn't exist. Use the
/// [`OpenOptions::create`] method to do so.
///
/// [`write()`]: Write::write "io::Write::write"
/// [`flush()`]: Write::flush "io::Write::flush"
/// [stream_position]: Seek::stream_position "io::Seek::stream_position"
/// [seek]: Seek::seek "io::Seek::seek"
/// [Current]: SeekFrom::Current "io::SeekFrom::Current"
/// [End]: SeekFrom::End "io::SeekFrom::End"
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().append(true).open("foo.txt");
/// ```
pub fn append(&mut self, append: bool) -> &mut Self {
self.append = append;
self
}
/// Sets the option for truncating a previous file.
///
/// If a file is successfully opened with this option set it will truncate
/// the file to 0 length if it already exists.
///
/// The file must be opened with write access for truncate to work.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().write(true).truncate(true).open("foo.txt");
/// ```
pub fn truncate(&mut self, truncate: bool) -> &mut Self {
self.truncate = truncate;
self
}
/// Sets the option to create a new file, or open it if it already exists.
///
/// In order for the file to be created, [`OpenOptions::write`] or
/// [`OpenOptions::append`] access must be used.
///
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().write(true).create(true).open("foo.txt");
/// ```
pub fn create(&mut self, create: bool) -> &mut Self {
self.create = create;
self
}
/// Sets the option to create a new file, failing if it already exists.
///
/// No file is allowed to exist at the target location, also no (dangling) symlink. In this
/// way, if the call succeeds, the file returned is guaranteed to be new.
/// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`]
/// or another error based on the situation. See [`OpenOptions::open`] for a
/// non-exhaustive list of likely errors.
///
/// This option is useful because it is atomic. Otherwise between checking
/// whether a file exists and creating a new one, the file may have been
/// created by another process (a TOCTOU race condition / attack).
///
/// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are
/// ignored.
///
/// The file must be opened with write or append access in order to create
/// a new file.
///
/// [`.create()`]: OpenOptions::create
/// [`.truncate()`]: OpenOptions::truncate
/// [`AlreadyExists`]: io::ErrorKind::AlreadyExists
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().write(true)
/// .create_new(true)
/// .open("foo.txt");
/// ```
pub fn create_new(&mut self, create_new: bool) -> &mut Self {
self.create_new = create_new;
self
}
}
#[cfg(unix)]
impl std::os::unix::fs::OpenOptionsExt for OpenOptions {
fn custom_flags(&mut self, flags: i32) -> &mut Self {
self.custom_flags.replace(flags);
self
}
fn mode(&mut self, mode: u32) -> &mut Self {
self.mode.replace(mode);
self
}
}
impl OpenOptions {
#[cfg(target_os = "android")]
fn android_mode(&self) -> String {
let mut mode = String::new();
if self.read {
mode.push('r');
}
if self.write {
mode.push('w');
}
if self.truncate {
mode.push('t');
}
if self.append {
mode.push('a');
}
mode
}
}
impl<R: Runtime> Fs<R> {
pub fn read_to_string<P: Into<FilePath>>(&self, path: P) -> std::io::Result<String> {
let mut s = String::new();
self.open(
path,
OpenOptions {
read: true,
..Default::default()
},
)?
.read_to_string(&mut s)?;
Ok(s)
}
pub fn read<P: Into<FilePath>>(&self, path: P) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.open(
path,
OpenOptions {
read: true,
..Default::default()
},
)?
.read_to_end(&mut buf)?;
Ok(buf)
}
}
// implement ScopeObject here instead of in the scope module because it is also used on the build script
// and we don't want to add tauri as a build dependency
impl ScopeObject for scope::Entry {
@@ -55,6 +423,9 @@ impl ScopeObject for scope::Entry {
pub trait FsExt<R: Runtime> {
fn fs_scope(&self) -> &Scope;
fn try_fs_scope(&self) -> Option<&Scope>;
/// Cross platform file system APIs that also support manipulating Android files.
fn fs(&self) -> &Fs<R>;
}
impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
@@ -65,6 +436,10 @@ impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
fn try_fs_scope(&self) -> Option<&Scope> {
self.try_state::<Scope>().map(|s| s.inner())
}
fn fs(&self) -> &Fs<R> {
self.state::<Fs<R>>().inner()
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
@@ -104,6 +479,15 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
.config()
.as_ref()
.and_then(|c| c.require_literal_leading_dot);
#[cfg(target_os = "android")]
{
let fs = mobile::init(app, api)?;
app.manage(fs);
}
#[cfg(not(target_os = "android"))]
app.manage(Fs(app.clone()));
app.manage(scope);
Ok(())
})
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{
plugin::{PluginApi, PluginHandle},
AppHandle, Runtime,
};
use crate::{models::*, FilePath, OpenOptions};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "com.plugin.fs";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_fs);
// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Fs<R>> {
#[cfg(target_os = "android")]
let handle = api
.register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin")
.unwrap();
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_android - intent - send)?;
Ok(Fs(handle))
}
/// Access to the android-intent-send APIs.
pub struct Fs<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> Fs<R> {
pub fn open<P: Into<FilePath>>(
&self,
path: P,
opts: OpenOptions,
) -> std::io::Result<std::fs::File> {
match path.into() {
FilePath::Url(u) => self
.resolve_content_uri(u.to_string(), opts.android_mode())
.map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("failed to open file: {e}"),
)
}),
FilePath::Path(p) => {
// tauri::utils::platform::resources_dir() returns a PathBuf with the Android asset URI prefix
// we must resolve that file with the Android API
if p.strip_prefix(tauri::utils::platform::ANDROID_ASSET_PROTOCOL_URI_PREFIX)
.is_ok()
{
self.resolve_content_uri(p.to_string_lossy(), opts.android_mode())
.map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("failed to open file: {e}"),
)
})
} else {
std::fs::OpenOptions::from(opts).open(p)
}
}
}
}
#[cfg(target_os = "android")]
fn resolve_content_uri(
&self,
uri: impl Into<String>,
mode: impl Into<String>,
) -> crate::Result<std::fs::File> {
#[cfg(target_os = "android")]
{
let result = self.0.run_mobile_plugin::<GetFileDescriptorResponse>(
"getFileDescriptor",
GetFileDescriptorPayload {
uri: uri.into(),
mode: mode.into(),
},
)?;
if let Some(fd) = result.fd {
Ok(unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(fd)
})
} else {
todo!()
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetFileDescriptorPayload {
pub uri: String,
pub mode: String,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetFileDescriptorResponse {
pub fd: Option<i32>,
}
+3 -3
View File
@@ -7,7 +7,7 @@ use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileI
use serde::Deserialize;
use tauri::{
ipc::{Channel, CommandScope, GlobalScope},
path::{BaseDirectory, SafePathBuf},
path::BaseDirectory,
Manager, Resource, ResourceId, Runtime, Webview,
};
@@ -22,7 +22,7 @@ use std::{
};
use crate::{
commands::{resolve_path, CommandResult},
commands::{resolve_path, CommandResult, SafeFilePath},
scope::Entry,
};
@@ -83,7 +83,7 @@ pub struct WatchOptions {
#[tauri::command]
pub async fn watch<R: Runtime>(
webview: Webview<R>,
paths: Vec<SafePathBuf>,
paths: Vec<SafeFilePath>,
options: WatchOptions,
on_event: Channel<Event>,
global_scope: GlobalScope<Entry>,