#![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>;
#[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 {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn read(&mut self, read: bool) -> &mut Self {
self.read = read;
self
}
pub fn write(&mut self, write: bool) -> &mut Self {
self.write = write;
self
}
pub fn append(&mut self, append: bool) -> &mut Self {
self.append = append;
self
}
pub fn truncate(&mut self, truncate: bool) -> &mut Self {
self.truncate = truncate;
self
}
pub fn create(&mut self, create: bool) -> &mut Self {
self.create = create;
self
}
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)
}
}
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>,
}
#[cfg(target_os = "ios")]
pub(crate) struct SecurityScopedResources {
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)] pub(crate) fn is_tracked_manually(&self, _url: &str) -> bool {
false
}
#[allow(dead_code)] pub(crate) fn track_manually(&self, _url: String) {}
#[allow(dead_code)] pub(crate) fn remove(&self, _url: &str) {}
}
pub trait FsExt<R: Runtime> {
fn fs_scope(&self) -> tauri::fs::Scope;
fn try_fs_scope(&self) -> Option<tauri::fs::Scope>;
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()
}
}
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()
}