snapshot of https://github.com/tauri-apps/plugins-workspace.git (v2, 2026-09-23; blobs > 131072B omitted)
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
This commit is contained in:
92
plugins/fs/src/android.rs
Normal file
92
plugins/fs/src/android.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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, AppHandle, Runtime};
|
||||
|
||||
use crate::{models::*, FilePath, OpenOptions};
|
||||
|
||||
const PLUGIN_IDENTIFIER: &str = "com.plugin.fs";
|
||||
|
||||
/// Access to the file system APIs on Android.
|
||||
///
|
||||
/// In addition to regular file system paths, it can read `content://` URIs
|
||||
/// and Android asset paths by resolving them with the Android plugin implementation.
|
||||
///
|
||||
/// Retrieved with [`crate::FsExt::fs`].
|
||||
pub struct Fs<R: Runtime>(tauri::plugin::PluginHandle<R>);
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<Fs<R>> {
|
||||
let handle = api
|
||||
.register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin")
|
||||
.unwrap();
|
||||
Ok(Fs(handle))
|
||||
}
|
||||
|
||||
impl<R: Runtime> Fs<R> {
|
||||
/// Open a file.
|
||||
///
|
||||
/// # Platform-specific
|
||||
///
|
||||
/// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL.
|
||||
/// You must call `stop_accessing_security_scoped_resource` when you're done accessing the file.
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_content_uri(
|
||||
&self,
|
||||
uri: impl Into<String>,
|
||||
mode: impl Into<String>,
|
||||
) -> crate::Result<std::fs::File> {
|
||||
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 {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
1885
plugins/fs/src/commands.rs
Normal file
1885
plugins/fs/src/commands.rs
Normal file
File diff suppressed because it is too large
Load Diff
19
plugins/fs/src/config.rs
Normal file
19
plugins/fs/src/config.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// Whether or not paths that contain components that start with a `.`
|
||||
/// will require that `.` appears literally in the pattern; `*`, `?`, `**`,
|
||||
/// or `[...]` will not match. This is useful because such files are
|
||||
/// conventionally considered hidden on Unix systems and it might be
|
||||
/// desirable to skip them when listing files.
|
||||
///
|
||||
/// Defaults to `true` on Unix systems and `false` on Windows
|
||||
// dotfiles are not supposed to be exposed by default on unix
|
||||
pub require_literal_leading_dot: Option<bool>,
|
||||
}
|
||||
44
plugins/fs/src/desktop.rs
Normal file
44
plugins/fs/src/desktop.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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};
|
||||
|
||||
/// Access to the file system APIs on desktop.
|
||||
///
|
||||
/// Retrieved with [`crate::FsExt::fs`].
|
||||
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> {
|
||||
/// Open a file.
|
||||
///
|
||||
/// # Platform-specific
|
||||
///
|
||||
/// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL.
|
||||
/// You must call `stop_accessing_security_scoped_resource` when you're done accessing the file.
|
||||
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)
|
||||
}
|
||||
}
|
||||
51
plugins/fs/src/error.rs
Normal file
51
plugins/fs/src/error.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
/// Errors that can happen while using the file system plugin.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// JSON serialization or deserialization error.
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
/// Error from the Tauri APIs, usually raised while resolving a path or a scope entry.
|
||||
#[error(transparent)]
|
||||
Tauri(#[from] tauri::Error),
|
||||
/// Error from the underlying file system operation.
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
/// The path is denied by the plugin scope or is not allowed by it.
|
||||
#[error("forbidden path: {0}")]
|
||||
PathForbidden(PathBuf),
|
||||
/// Invalid glob pattern.
|
||||
#[error("invalid glob pattern: {0}")]
|
||||
GlobPattern(#[from] glob::PatternError),
|
||||
/// Watcher error.
|
||||
#[cfg(feature = "watch")]
|
||||
#[error(transparent)]
|
||||
Watch(#[from] notify::Error),
|
||||
/// Error invoking the Android plugin implementation.
|
||||
#[cfg(target_os = "android")]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
/// The URL cannot be converted to a file system path.
|
||||
#[error("URL is not a valid path")]
|
||||
InvalidPathUrl,
|
||||
/// The path is not safe to use, for instance because it traverses parent directories.
|
||||
#[error("Unsafe PathBuf: {0}")]
|
||||
UnsafePathBuf(&'static str),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
308
plugins/fs/src/file_path.rs
Normal file
308
plugins/fs/src/file_path.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::path::SafePathBuf;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Represents either a filesystem path or a URI pointing to a file
|
||||
/// such as `file://` URIs or Android `content://` URIs.
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum FilePath {
|
||||
/// `file://` URIs or Android `content://` URIs.
|
||||
Url(url::Url),
|
||||
/// Regular [`PathBuf`]
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
/// Represents either a safe filesystem path or a URI pointing to a file
|
||||
/// such as `file://` URIs or Android `content://` URIs.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum SafeFilePath {
|
||||
/// `file://` URIs or Android `content://` URIs.
|
||||
Url(url::Url),
|
||||
/// Safe [`PathBuf`], see [`SafePathBuf``].
|
||||
Path(SafePathBuf),
|
||||
}
|
||||
|
||||
impl FilePath {
|
||||
/// Get a reference to the contained [`Path`] if the variant is [`FilePath::Path`].
|
||||
///
|
||||
/// Use [`FilePath::into_path`] to try to convert the [`FilePath::Url`] variant as well.
|
||||
#[inline]
|
||||
pub fn as_path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::Url(_) => None,
|
||||
Self::Path(p) => Some(p),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert into [`PathBuf`] if possible.
|
||||
///
|
||||
/// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`FilePath::Url`],
|
||||
/// otherwise returns the contained [PathBuf] as is.
|
||||
#[inline]
|
||||
pub fn into_path(self) -> Result<PathBuf> {
|
||||
match self {
|
||||
Self::Url(url) => url.to_file_path().map_err(|_| Error::InvalidPathUrl),
|
||||
Self::Path(p) => Ok(p),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the contained [`PathBuf`] if the variant is [`FilePath::Path`],
|
||||
/// and when possible, converts Windows UNC paths to regular paths.
|
||||
#[inline]
|
||||
pub fn simplified(self) -> Self {
|
||||
match self {
|
||||
Self::Url(url) => Self::Url(url),
|
||||
Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SafeFilePath {
|
||||
/// Get a reference to the contained [`Path`] if the variant is [`SafeFilePath::Path`].
|
||||
///
|
||||
/// Use [`SafeFilePath::into_path`] to try to convert the [`SafeFilePath::Url`] variant as well.
|
||||
#[inline]
|
||||
pub fn as_path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::Url(_) => None,
|
||||
Self::Path(p) => Some(p.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert into [`PathBuf`] if possible.
|
||||
///
|
||||
/// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`SafeFilePath::Url`],
|
||||
/// otherwise returns the contained [PathBuf] as is.
|
||||
#[inline]
|
||||
pub fn into_path(self) -> Result<PathBuf> {
|
||||
match self {
|
||||
Self::Url(url) => url.to_file_path().map_err(|_| Error::InvalidPathUrl),
|
||||
Self::Path(p) => Ok(p.as_ref().to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the contained [`PathBuf`] if the variant is [`SafeFilePath::Path`],
|
||||
/// and when possible, converts Windows UNC paths to regular paths.
|
||||
#[inline]
|
||||
pub fn simplified(self) -> Self {
|
||||
match self {
|
||||
Self::Url(url) => Self::Url(url),
|
||||
Self::Path(p) => {
|
||||
// Safe to unwrap since it was a safe file path already
|
||||
Self::Path(SafePathBuf::new(dunce::simplified(p.as_ref()).to_path_buf()).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FilePath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Url(u) => u.fmt(f),
|
||||
Self::Path(p) => p.display().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SafeFilePath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Url(u) => u.fmt(f),
|
||||
Self::Path(p) => p.display().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for FilePath {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct FilePathVisitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for FilePathVisitor {
|
||||
type Value = FilePath;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string representing an file URL or a path")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
FilePath::from_str(s).map_err(|e| {
|
||||
serde::de::Error::invalid_value(
|
||||
serde::de::Unexpected::Str(s),
|
||||
&e.to_string().as_str(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(FilePathVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for SafeFilePath {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct SafeFilePathVisitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for SafeFilePathVisitor {
|
||||
type Value = SafeFilePath;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string representing an file URL or a path")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
SafeFilePath::from_str(s).map_err(|e| {
|
||||
serde::de::Error::invalid_value(
|
||||
serde::de::Unexpected::Str(s),
|
||||
&e.to_string().as_str(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(SafeFilePathVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if url.scheme().len() != 1 {
|
||||
return Ok(Self::Url(url));
|
||||
}
|
||||
}
|
||||
Ok(Self::Path(PathBuf::from(s)))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SafeFilePath {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
if let Ok(url) = url::Url::from_str(s) {
|
||||
if url.scheme().len() != 1 {
|
||||
return Ok(Self::Url(url));
|
||||
}
|
||||
}
|
||||
|
||||
SafePathBuf::new(s.into())
|
||||
.map(SafeFilePath::Path)
|
||||
.map_err(Error::UnsafePathBuf)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for FilePath {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
Self::Path(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PathBuf> for SafeFilePath {
|
||||
type Error = Error;
|
||||
fn try_from(value: PathBuf) -> Result<Self> {
|
||||
SafePathBuf::new(value)
|
||||
.map(SafeFilePath::Path)
|
||||
.map_err(Error::UnsafePathBuf)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for FilePath {
|
||||
fn from(value: &Path) -> Self {
|
||||
Self::Path(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Path> for SafeFilePath {
|
||||
type Error = Error;
|
||||
fn try_from(value: &Path) -> Result<Self> {
|
||||
SafePathBuf::new(value.to_path_buf())
|
||||
.map(SafeFilePath::Path)
|
||||
.map_err(Error::UnsafePathBuf)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PathBuf> for FilePath {
|
||||
fn from(value: &PathBuf) -> Self {
|
||||
Self::Path(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&PathBuf> for SafeFilePath {
|
||||
type Error = Error;
|
||||
fn try_from(value: &PathBuf) -> Result<Self> {
|
||||
SafePathBuf::new(value.to_owned())
|
||||
.map(SafeFilePath::Path)
|
||||
.map_err(Error::UnsafePathBuf)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::Url> for FilePath {
|
||||
fn from(value: url::Url) -> Self {
|
||||
Self::Url(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::Url> for SafeFilePath {
|
||||
fn from(value: url::Url) -> Self {
|
||||
Self::Url(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FilePath> for PathBuf {
|
||||
type Error = Error;
|
||||
fn try_from(value: FilePath) -> Result<Self> {
|
||||
value.into_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<SafeFilePath> for PathBuf {
|
||||
type Error = Error;
|
||||
fn try_from(value: SafeFilePath) -> Result<Self> {
|
||||
value.into_path()
|
||||
}
|
||||
}
|
||||
|
||||
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 TryFrom<FilePath> for SafeFilePath {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: FilePath) -> Result<Self> {
|
||||
match value {
|
||||
FilePath::Url(url) => Ok(SafeFilePath::Url(url)),
|
||||
FilePath::Path(p) => SafePathBuf::new(p)
|
||||
.map(SafeFilePath::Path)
|
||||
.map_err(Error::UnsafePathBuf),
|
||||
}
|
||||
}
|
||||
}
|
||||
142
plugins/fs/src/ios.rs
Normal file
142
plugins/fs/src/ios.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
// 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, AppHandle, Runtime};
|
||||
|
||||
use crate::{FilePath, OpenOptions};
|
||||
|
||||
/// Access to the file system APIs on iOS.
|
||||
///
|
||||
/// Opening a `file://` URL automatically starts accessing the matching security-scoped resource.
|
||||
///
|
||||
/// Retrieved with [`crate::FsExt::fs`].
|
||||
pub struct Fs<R: Runtime> {
|
||||
_phantom: std::marker::PhantomData<fn() -> R>,
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<Fs<R>> {
|
||||
Ok(Fs {
|
||||
_phantom: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
impl<R: Runtime> Fs<R> {
|
||||
/// Open a file.
|
||||
///
|
||||
/// # Platform-specific
|
||||
///
|
||||
/// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL.
|
||||
/// You must call [`Self::stop_accessing_security_scoped_resource`] when you're done accessing the file.
|
||||
pub fn open<P: Into<FilePath>>(
|
||||
&self,
|
||||
path: P,
|
||||
opts: OpenOptions,
|
||||
) -> std::io::Result<std::fs::File> {
|
||||
use objc2_foundation::{NSString, NSURL};
|
||||
|
||||
match path.into() {
|
||||
FilePath::Url(url) if url.scheme() == "file" => {
|
||||
// Handle security-scoped URLs on iOS
|
||||
let url_string = url.as_str();
|
||||
let url_nsstring = NSString::from_str(url_string);
|
||||
|
||||
// Create NSURL from the URL string
|
||||
// URLWithString may return None for invalid URLs, but file:// URLs should be valid
|
||||
let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) };
|
||||
if let Some(ns_url) = ns_url {
|
||||
// Start accessing the security-scoped resource
|
||||
// This is required for files outside the app's sandbox (e.g., from file picker)
|
||||
// Note: We don't call stopAccessingSecurityScopedResource here because
|
||||
// the file handle needs to remain accessible while the File is in use.
|
||||
// The access will be automatically stopped when the app is backgrounded or terminated.
|
||||
unsafe {
|
||||
let success = ns_url.startAccessingSecurityScopedResource();
|
||||
if success {
|
||||
log::debug!(
|
||||
"Started accessing security-scoped resource for URL: {}",
|
||||
url_string
|
||||
);
|
||||
} else {
|
||||
log::warn!(
|
||||
"Failed to start accessing security-scoped resource for URL: {}",
|
||||
url_string
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url_string);
|
||||
}
|
||||
|
||||
// Convert URL to path and open the file
|
||||
let path = url.to_file_path().map_err(|_| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL")
|
||||
})?;
|
||||
std::fs::OpenOptions::from(opts).open(path)
|
||||
}
|
||||
FilePath::Url(_) => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"cannot use a non-file URL to load files on iOS",
|
||||
)),
|
||||
FilePath::Path(p) => {
|
||||
// Regular path, no security-scoped resource handling needed
|
||||
std::fs::OpenOptions::from(opts).open(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops accessing a security-scoped resource for the given file path or URL.
|
||||
/// This should be called when you're done accessing a file that was opened
|
||||
/// using a security-scoped URL (e.g., from a file picker).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - A file path or URL that was previously accessed via `startAccessingSecurityScopedResource`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(())` if successful, or an error if the path/URL is invalid or not a file URL.
|
||||
pub fn stop_accessing_security_scoped_resource<P: Into<FilePath>>(
|
||||
&self,
|
||||
path: P,
|
||||
) -> crate::Result<()> {
|
||||
use objc2_foundation::{NSString, NSURL};
|
||||
|
||||
let file_path = path.into();
|
||||
let url_string = match file_path {
|
||||
FilePath::Url(url) => {
|
||||
if url.scheme() != "file" {
|
||||
return Err(crate::Error::InvalidPathUrl);
|
||||
}
|
||||
url.as_str().to_string()
|
||||
}
|
||||
FilePath::Path(p) => {
|
||||
// Convert path to file URL
|
||||
url::Url::from_file_path(&p)
|
||||
.map_err(|_| crate::Error::InvalidPathUrl)?
|
||||
.as_str()
|
||||
.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let url_nsstring = NSString::from_str(&url_string);
|
||||
let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) };
|
||||
if let Some(ns_url) = ns_url {
|
||||
// Stop accessing the security-scoped resource
|
||||
unsafe {
|
||||
ns_url.stopAccessingSecurityScopedResource();
|
||||
}
|
||||
} else {
|
||||
return Err(crate::Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"failed to create NSURL from URL",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
632
plugins/fs/src/lib.rs
Normal file
632
plugins/fs/src/lib.rs
Normal file
@@ -0,0 +1,632 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Access the file system.
|
||||
//!
|
||||
//! ## Cargo features
|
||||
//!
|
||||
//! - **watch**: Enables the `watch` command backed by [`notify`](http://crates.io/crates/notify).
|
||||
|
||||
// TODO(v3): consider redesign the API to implement automatic stopAccessingSecurityScopedResource on iOS
|
||||
// this likely requires returning a handle to a resource so we can impl Drop for it
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
|
||||
)]
|
||||
|
||||
use std::io::Read;
|
||||
#[cfg(target_os = "ios")]
|
||||
use std::sync::Mutex;
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::{
|
||||
ipc::ScopeObject,
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
utils::{acl::Value, config::FsScope},
|
||||
AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android;
|
||||
mod commands;
|
||||
mod config;
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
mod error;
|
||||
mod file_path;
|
||||
#[cfg(target_os = "ios")]
|
||||
mod ios;
|
||||
#[cfg(target_os = "android")]
|
||||
mod models;
|
||||
mod scope;
|
||||
#[cfg(feature = "watch")]
|
||||
mod watcher;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub use android::Fs;
|
||||
#[cfg(desktop)]
|
||||
pub use desktop::Fs;
|
||||
#[cfg(target_os = "ios")]
|
||||
pub use ios::Fs;
|
||||
|
||||
pub use error::Error;
|
||||
|
||||
pub use file_path::FilePath;
|
||||
pub use file_path::SafeFilePath;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Options and flags which can be used to configure how a file is opened.
|
||||
///
|
||||
/// This builder exposes the ability to configure how a [`std::fs::File`] is opened and
|
||||
/// what operations are permitted on the open file. Build it with [`OpenOptions::new`],
|
||||
/// chain calls to the setter methods and pass it to [`Fs::open`].
|
||||
///
|
||||
/// The `read` option defaults to `true`, every other option defaults to `false`.
|
||||
#[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)]
|
||||
#[allow(unused)]
|
||||
mode: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[allow(unused)]
|
||||
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
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::path::Path;
|
||||
/// use tauri_plugin_fs::{FsExt, OpenOptions};
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let mut options = OpenOptions::new();
|
||||
/// options.read(true);
|
||||
/// let file = app.fs().open(Path::new("foo.txt"), options)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
#[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
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::path::Path;
|
||||
/// use tauri_plugin_fs::{FsExt, OpenOptions};
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let mut options = OpenOptions::new();
|
||||
/// options.read(true);
|
||||
/// let file = app.fs().open(Path::new("foo.txt"), options)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
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
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::path::Path;
|
||||
/// use tauri_plugin_fs::{FsExt, OpenOptions};
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let mut options = OpenOptions::new();
|
||||
/// options.write(true);
|
||||
/// let file = app.fs().open(Path::new("foo.txt"), options)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
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()`]: std::io::Write::write "io::Write::write"
|
||||
/// [`flush()`]: std::io::Write::flush "io::Write::flush"
|
||||
/// [Seek]: std::io::Seek "io::Seek"
|
||||
/// [stream_position]: std::io::Seek::stream_position "io::Seek::stream_position"
|
||||
/// [seek]: std::io::Seek::seek "io::Seek::seek"
|
||||
/// [SeekFrom]: std::io::SeekFrom "io::SeekFrom"
|
||||
/// [Current]: std::io::SeekFrom::Current "io::SeekFrom::Current"
|
||||
/// [End]: std::io::SeekFrom::End "io::SeekFrom::End"
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::path::Path;
|
||||
/// use tauri_plugin_fs::{FsExt, OpenOptions};
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let mut options = OpenOptions::new();
|
||||
/// options.append(true);
|
||||
/// let file = app.fs().open(Path::new("foo.txt"), options)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
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
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::path::Path;
|
||||
/// use tauri_plugin_fs::{FsExt, OpenOptions};
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let mut options = OpenOptions::new();
|
||||
/// options.write(true).truncate(true);
|
||||
/// let file = app.fs().open(Path::new("foo.txt"), options)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
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
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::path::Path;
|
||||
/// use tauri_plugin_fs::{FsExt, OpenOptions};
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let mut options = OpenOptions::new();
|
||||
/// options.write(true).create(true);
|
||||
/// let file = app.fs().open(Path::new("foo.txt"), options)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
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 [`std::fs::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`]: std::io::ErrorKind::AlreadyExists
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::path::Path;
|
||||
/// use tauri_plugin_fs::{FsExt, OpenOptions};
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let mut options = OpenOptions::new();
|
||||
/// options.write(true).create_new(true);
|
||||
/// let file = app.fs().open(Path::new("foo.txt"), options)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
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> {
|
||||
/// Reads the entire contents of a file into a string.
|
||||
///
|
||||
/// The file is opened in read-only mode with [`Fs::open`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `path` cannot be opened for reading or if its
|
||||
/// contents are not valid UTF-8.
|
||||
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)
|
||||
}
|
||||
|
||||
/// Reads the entire contents of a file into a bytes vector.
|
||||
///
|
||||
/// The file is opened in read-only mode with [`Fs::open`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `path` cannot be opened for reading.
|
||||
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 {
|
||||
type Error = Error;
|
||||
fn deserialize<R: Runtime>(
|
||||
app: &AppHandle<R>,
|
||||
raw: Value,
|
||||
) -> std::result::Result<Self, Self::Error> {
|
||||
let path = serde_json::from_value(raw.into()).map(|raw| match raw {
|
||||
scope::EntryRaw::Value(path) => path,
|
||||
scope::EntryRaw::Object { path } => path,
|
||||
})?;
|
||||
|
||||
match app.path().parse(path) {
|
||||
Ok(path) => Ok(Self { path: Some(path) }),
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Err(tauri::Error::UnknownPath) => Ok(Self { path: None }),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Scope {
|
||||
pub(crate) scope: tauri::fs::Scope,
|
||||
pub(crate) require_literal_leading_dot: Option<bool>,
|
||||
}
|
||||
|
||||
/// Tracks which paths have active security-scoped resource access on iOS.
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) struct SecurityScopedResources {
|
||||
/// Set of file URLs that are currently accessing security-scoped resources.
|
||||
/// The key is the URL string representation.
|
||||
pub(crate) active_urls: Mutex<std::collections::HashSet<String>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
impl SecurityScopedResources {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
active_urls: Mutex::new(std::collections::HashSet::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_tracked_manually(&self, url: &str) -> bool {
|
||||
self.active_urls.lock().unwrap().contains(url)
|
||||
}
|
||||
|
||||
pub(crate) fn track_manually(&self, url: String) {
|
||||
self.active_urls.lock().unwrap().insert(url);
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&self, url: &str) {
|
||||
self.active_urls.lock().unwrap().remove(url);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub(crate) struct SecurityScopedResources;
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
impl SecurityScopedResources {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Used on iOS, but not on other platforms
|
||||
pub(crate) fn is_tracked_manually(&self, _url: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Used on iOS, but not on other platforms
|
||||
pub(crate) fn track_manually(&self, _url: String) {}
|
||||
|
||||
#[allow(dead_code)] // Used on iOS, but not on other platforms
|
||||
pub(crate) fn remove(&self, _url: &str) {}
|
||||
}
|
||||
|
||||
/// Extension trait implemented by every [`Manager`] (the app handle, windows, webviews, ...)
|
||||
/// to access the file system plugin APIs.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::path::Path;
|
||||
/// use tauri::Runtime;
|
||||
/// use tauri_plugin_fs::FsExt;
|
||||
///
|
||||
/// fn setup<R: Runtime>(app: &tauri::App<R>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// // allow the app to access a directory that is not part of the static scope
|
||||
/// app.fs_scope().allow_directory(Path::new("/path/to/directory"), true)?;
|
||||
///
|
||||
/// let contents = app.fs().read_to_string(Path::new("/path/to/directory/file.txt"))?;
|
||||
/// println!("{contents}");
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub trait FsExt<R: Runtime> {
|
||||
/// Returns the file system scope, which can be used to dynamically
|
||||
/// allow or deny paths at runtime.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the plugin is not registered in the app.
|
||||
/// Use [`FsExt::try_fs_scope`] if the plugin might not be registered.
|
||||
fn fs_scope(&self) -> tauri::fs::Scope;
|
||||
|
||||
/// Returns the file system scope, or `None` if the plugin is not registered in the app.
|
||||
fn try_fs_scope(&self) -> Option<tauri::fs::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 {
|
||||
fn fs_scope(&self) -> tauri::fs::Scope {
|
||||
self.state::<Scope>().scope.clone()
|
||||
}
|
||||
|
||||
fn try_fs_scope(&self) -> Option<tauri::fs::Scope> {
|
||||
self.try_state::<Scope>().map(|s| s.scope.clone())
|
||||
}
|
||||
|
||||
fn fs(&self) -> &Fs<R> {
|
||||
self.state::<Fs<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
||||
PluginBuilder::<R, Option<config::Config>>::new("fs")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::create,
|
||||
commands::open,
|
||||
commands::copy_file,
|
||||
commands::mkdir,
|
||||
commands::read_dir,
|
||||
commands::read,
|
||||
commands::read_file,
|
||||
commands::read_text_file,
|
||||
commands::read_text_file_lines,
|
||||
commands::read_text_file_lines_next,
|
||||
commands::remove,
|
||||
commands::rename,
|
||||
commands::seek,
|
||||
commands::stat,
|
||||
commands::lstat,
|
||||
commands::fstat,
|
||||
commands::truncate,
|
||||
commands::ftruncate,
|
||||
commands::write,
|
||||
commands::write_file,
|
||||
commands::write_text_file,
|
||||
commands::exists,
|
||||
commands::size,
|
||||
commands::start_accessing_security_scoped_resource,
|
||||
commands::stop_accessing_security_scoped_resource,
|
||||
#[cfg(feature = "watch")]
|
||||
watcher::watch,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
let scope = Scope {
|
||||
require_literal_leading_dot: api
|
||||
.config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.require_literal_leading_dot),
|
||||
scope: tauri::fs::Scope::new(app, &FsScope::default())?,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let fs = android::init(app, api)?;
|
||||
app.manage(fs);
|
||||
}
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
let fs = ios::init(app, api)?;
|
||||
app.manage(fs);
|
||||
}
|
||||
#[cfg(desktop)]
|
||||
app.manage(Fs(app.clone()));
|
||||
|
||||
app.manage(scope);
|
||||
app.manage(SecurityScopedResources::new());
|
||||
Ok(())
|
||||
})
|
||||
.on_event(|app, event| {
|
||||
if let RunEvent::WindowEvent {
|
||||
label: _,
|
||||
event: WindowEvent::DragDrop(DragDropEvent::Drop { paths, position: _ }),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let scope = app.fs_scope();
|
||||
for path in paths {
|
||||
if path.is_file() {
|
||||
let _ = scope.allow_file(path);
|
||||
} else {
|
||||
let _ = scope.allow_directory(path, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
18
plugins/fs/src/models.rs
Normal file
18
plugins/fs/src/models.rs
Normal 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>,
|
||||
}
|
||||
19
plugins/fs/src/scope.rs
Normal file
19
plugins/fs/src/scope.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Entry {
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum EntryRaw {
|
||||
Value(PathBuf),
|
||||
Object { path: PathBuf },
|
||||
}
|
||||
104
plugins/fs/src/watcher.rs
Normal file
104
plugins/fs/src/watcher.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use notify_debouncer_full::{new_debouncer, DebouncedEvent, Debouncer, RecommendedCache};
|
||||
use serde::Deserialize;
|
||||
use tauri::{
|
||||
ipc::{Channel, CommandScope, GlobalScope},
|
||||
path::BaseDirectory,
|
||||
Manager, Resource, ResourceId, Runtime, Webview,
|
||||
};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
commands::{resolve_path, CommandResult},
|
||||
scope::Entry,
|
||||
SafeFilePath,
|
||||
};
|
||||
|
||||
#[allow(unused)]
|
||||
enum WatcherKind {
|
||||
Debouncer(Debouncer<RecommendedWatcher, RecommendedCache>),
|
||||
Watcher(RecommendedWatcher),
|
||||
}
|
||||
|
||||
impl Resource for WatcherKind {}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WatchOptions {
|
||||
base_dir: Option<BaseDirectory>,
|
||||
#[serde(default)]
|
||||
recursive: bool,
|
||||
delay_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn watch<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
paths: Vec<SafeFilePath>,
|
||||
options: WatchOptions,
|
||||
on_event: Channel<notify::Event>,
|
||||
global_scope: GlobalScope<Entry>,
|
||||
command_scope: CommandScope<Entry>,
|
||||
) -> CommandResult<ResourceId> {
|
||||
let resolved_paths = paths
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
resolve_path(
|
||||
"watch",
|
||||
&webview,
|
||||
&global_scope,
|
||||
&command_scope,
|
||||
path,
|
||||
options.base_dir,
|
||||
)
|
||||
})
|
||||
.collect::<CommandResult<Vec<_>>>()?;
|
||||
|
||||
let recursive_mode = if options.recursive {
|
||||
RecursiveMode::Recursive
|
||||
} else {
|
||||
RecursiveMode::NonRecursive
|
||||
};
|
||||
|
||||
let watcher_kind = if let Some(delay) = options.delay_ms {
|
||||
let mut debouncer = new_debouncer(
|
||||
Duration::from_millis(delay),
|
||||
None,
|
||||
move |events: Result<Vec<DebouncedEvent>, Vec<notify::Error>>| {
|
||||
if let Ok(events) = events {
|
||||
for event in events {
|
||||
// TODO: Should errors be emitted too?
|
||||
let _ = on_event.send(event.event);
|
||||
}
|
||||
}
|
||||
},
|
||||
)?;
|
||||
for path in &resolved_paths {
|
||||
debouncer.watch(path, recursive_mode)?;
|
||||
}
|
||||
WatcherKind::Debouncer(debouncer)
|
||||
} else {
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |event| {
|
||||
if let Ok(event) = event {
|
||||
// TODO: Should errors be emitted too?
|
||||
let _ = on_event.send(event);
|
||||
}
|
||||
},
|
||||
Config::default(),
|
||||
)?;
|
||||
for path in &resolved_paths {
|
||||
watcher.watch(path, recursive_mode)?;
|
||||
}
|
||||
WatcherKind::Watcher(watcher)
|
||||
};
|
||||
|
||||
let rid = webview.resources_table().add(watcher_kind);
|
||||
|
||||
Ok(rid)
|
||||
}
|
||||
Reference in New Issue
Block a user