From 158641d70d7d095ba26592a7b7f11ac8c9981b6e Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Wed, 11 Sep 2024 00:02:23 +0300 Subject: [PATCH] --- .../java/ExampleInstrumentedTest.kt | 28 - android/src/main/AndroidManifest.xml | 3 - android/src/main/java/FsPlugin.kt | 93 -- android/src/test/java/ExampleUnitTest.kt | 21 - package.json | 52 +- src/commands.rs | 1154 ----------------- src/config.rs | 19 - src/desktop.rs | 35 - src/error.rs | 43 - src/file_path.rs | 314 ----- src/lib.rs | 459 ------- src/mobile.rs | 96 -- src/models.rs | 18 - src/scope.rs | 132 -- src/watcher.rs | 159 --- 15 files changed, 24 insertions(+), 2602 deletions(-) delete mode 100644 android/src/androidTest/java/ExampleInstrumentedTest.kt delete mode 100644 android/src/main/AndroidManifest.xml delete mode 100644 android/src/main/java/FsPlugin.kt delete mode 100644 android/src/test/java/ExampleUnitTest.kt delete mode 100644 src/commands.rs delete mode 100644 src/config.rs delete mode 100644 src/desktop.rs delete mode 100644 src/error.rs delete mode 100644 src/file_path.rs delete mode 100644 src/lib.rs delete mode 100644 src/mobile.rs delete mode 100644 src/models.rs delete mode 100644 src/scope.rs delete mode 100644 src/watcher.rs diff --git a/android/src/androidTest/java/ExampleInstrumentedTest.kt b/android/src/androidTest/java/ExampleInstrumentedTest.kt deleted file mode 100644 index c3b473f..0000000 --- a/android/src/androidTest/java/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -package com.plugin.fs - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.plugin.fs", appContext.packageName) - } -} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml deleted file mode 100644 index 9a40236..0000000 --- a/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/android/src/main/java/FsPlugin.kt b/android/src/main/java/FsPlugin.kt deleted file mode 100644 index 877fbf4..0000000 --- a/android/src/main/java/FsPlugin.kt +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -package com.plugin.fs - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.res.AssetManager.ACCESS_BUFFER -import android.net.Uri -import android.os.ParcelFileDescriptor -import app.tauri.annotation.Command -import app.tauri.annotation.InvokeArg -import app.tauri.annotation.TauriPlugin -import app.tauri.plugin.Invoke -import app.tauri.plugin.JSObject -import app.tauri.plugin.Plugin -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream - -@InvokeArg -class WriteTextFileArgs { - val uri: String = "" - val content: String = "" -} - -@InvokeArg -class GetFileDescriptorArgs { - lateinit var uri: String - lateinit var mode: String -} - -@TauriPlugin -class FsPlugin(private val activity: Activity): Plugin(activity) { - @SuppressLint("Recycle") - @Command - fun getFileDescriptor(invoke: Invoke) { - val args = invoke.parseArgs(GetFileDescriptorArgs::class.java) - - val res = JSObject() - - if (args.uri.startsWith(app.tauri.TAURI_ASSETS_DIRECTORY_URI)) { - val path = args.uri.substring(app.tauri.TAURI_ASSETS_DIRECTORY_URI.length) - try { - val fd = activity.assets.openFd(path).parcelFileDescriptor?.detachFd() - res.put("fd", fd) - } catch (e: IOException) { - // if the asset is compressed, we cannot open a file descriptor directly - // so we copy it to the cache and get a fd from there - // this is a lot faster than serializing the file and sending it as invoke response - // because on the Rust side we can leverage the custom protocol IPC and read the file directly - val cacheFile = File(activity.cacheDir, "_assets/$path") - cacheFile.parentFile?.mkdirs() - copyAsset(path, cacheFile) - - val fd = ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.parseMode(args.mode)).detachFd() - res.put("fd", fd) - } - } else { - val fd = activity.contentResolver.openAssetFileDescriptor( - Uri.parse(args.uri), - args.mode - )?.parcelFileDescriptor?.detachFd() - res.put("fd", fd) - } - - invoke.resolve(res) - } - - @Throws(IOException::class) - private fun copy(input: InputStream, output: OutputStream) { - val buf = ByteArray(1024) - var len: Int - while ((input.read(buf).also { len = it }) > 0) { - output.write(buf, 0, len) - } - } - - @Throws(IOException::class) - private fun copyAsset(assetPath: String, cacheFile: File) { - val input = activity.assets.open(assetPath, ACCESS_BUFFER) - input.use { i -> - val output = FileOutputStream(cacheFile, false) - output.use { o -> - copy(i, o) - } - } - } -} - diff --git a/android/src/test/java/ExampleUnitTest.kt b/android/src/test/java/ExampleUnitTest.kt deleted file mode 100644 index 340839a..0000000 --- a/android/src/test/java/ExampleUnitTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -package com.plugin.fs - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/package.json b/package.json index 7fd93a0..a334321 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,26 @@ { - "name": "@tauri-apps/plugin-fs", - "version": "2.0.0-rc.2", - "description": "Access the file system.", - "license": "MIT or APACHE-2.0", - "authors": [ - "Tauri Programme within The Commons Conservancy" - ], - "repository": "https://github.com/tauri-apps/plugins-workspace", - "type": "module", - "types": "./dist-js/index.d.ts", - "main": "./dist-js/index.cjs", - "module": "./dist-js/index.js", - "exports": { - "types": "./dist-js/index.d.ts", - "import": "./dist-js/index.js", - "require": "./dist-js/index.cjs" - }, - "scripts": { - "build": "rollup -c" - }, - "files": [ - "dist-js", - "README.md", - "LICENSE" - ], - "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.4" - } + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "dependencies": { + "@tauri-apps/api": "^2.0.0-rc.4" + }, + "description": "Access the file system.", + "exports": { + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs", + "types": "./dist-js/index.d.ts" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "name": "@tauri-apps/plugin-fs", + "scripts": { + "build": "rollup -c" + }, + "types": "./dist-js/index.d.ts" } diff --git a/src/commands.rs b/src/commands.rs deleted file mode 100644 index b72e3b3..0000000 --- a/src/commands.rs +++ /dev/null @@ -1,1154 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// Copyright 2018-2023 the Deno authors. -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use serde::{Deserialize, Serialize, Serializer}; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use tauri::{ - ipc::{CommandScope, GlobalScope}, - path::BaseDirectory, - utils::config::FsScope, - AppHandle, Manager, Resource, ResourceId, Runtime, Webview, -}; - -use std::{ - borrow::Cow, - fs::File, - io::{BufReader, Lines, Read, Write}, - path::{Path, PathBuf}, - str::FromStr, - sync::Mutex, - time::{SystemTime, UNIX_EPOCH}, -}; - -use crate::{scope::Entry, Error, FsExt, SafeFilePath}; - -#[derive(Debug, thiserror::Error)] -pub enum CommandError { - #[error(transparent)] - Anyhow(#[from] anyhow::Error), - #[error(transparent)] - Plugin(#[from] Error), - #[error(transparent)] - Tauri(#[from] tauri::Error), - #[error(transparent)] - Json(#[from] serde_json::Error), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - UrlParseError(#[from] url::ParseError), - #[cfg(feature = "watch")] - #[error(transparent)] - Watcher(#[from] notify::Error), -} - -impl From for CommandError { - fn from(value: String) -> Self { - Self::Anyhow(anyhow::anyhow!(value)) - } -} - -impl From<&str> for CommandError { - fn from(value: &str) -> Self { - Self::Anyhow(anyhow::anyhow!(value.to_string())) - } -} - -impl Serialize for CommandError { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - if let Self::Anyhow(err) = self { - serializer.serialize_str(format!("{err:#}").as_ref()) - } else { - serializer.serialize_str(self.to_string().as_ref()) - } - } -} - -pub type CommandResult = std::result::Result; - -#[derive(Debug, Default, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BaseOptions { - base_dir: Option, -} - -#[tauri::command] -pub fn create( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.and_then(|o| o.base_dir), - )?; - let file = File::create(&resolved_path).map_err(|e| { - format!( - "failed to create file at path: {} with error: {e}", - resolved_path.display() - ) - })?; - let rid = webview.resources_table().add(StdFileResource::new(file)); - Ok(rid) -} - -#[derive(Debug, Default, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OpenOptions { - #[serde(flatten)] - base: BaseOptions, - #[serde(flatten)] - options: crate::OpenOptions, -} - -#[tauri::command] -pub fn open( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let (file, _path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - if let Some(opts) = options { - OpenOptions { - base: opts.base, - options: opts.options, - } - } 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)); - - Ok(rid) -} - -#[tauri::command] -pub fn close(webview: Webview, rid: ResourceId) -> CommandResult<()> { - webview.resources_table().close(rid).map_err(Into::into) -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CopyFileOptions { - from_path_base_dir: Option, - to_path_base_dir: Option, -} - -#[tauri::command] -pub async fn copy_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - from_path: SafeFilePath, - to_path: SafeFilePath, - options: Option, -) -> CommandResult<()> { - let resolved_from_path = resolve_path( - &webview, - &global_scope, - &command_scope, - from_path, - options.as_ref().and_then(|o| o.from_path_base_dir), - )?; - let resolved_to_path = resolve_path( - &webview, - &global_scope, - &command_scope, - to_path, - options.as_ref().and_then(|o| o.to_path_base_dir), - )?; - std::fs::copy(&resolved_from_path, &resolved_to_path).map_err(|e| { - format!( - "failed to copy file from path: {}, to path: {} with error: {e}", - resolved_from_path.display(), - resolved_to_path.display() - ) - })?; - Ok(()) -} - -#[derive(Debug, Clone, Deserialize)] -pub struct MkdirOptions { - #[serde(flatten)] - base: BaseOptions, - #[allow(unused)] - mode: Option, - recursive: Option, -} - -#[tauri::command] -pub fn mkdir( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult<()> { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base.base_dir), - )?; - - let mut builder = std::fs::DirBuilder::new(); - builder.recursive(options.as_ref().and_then(|o| o.recursive).unwrap_or(false)); - - #[cfg(unix)] - { - use std::os::unix::fs::DirBuilderExt; - let mode = options.as_ref().and_then(|o| o.mode).unwrap_or(0o777) & 0o777; - builder.mode(mode); - } - - builder - .create(&resolved_path) - .map_err(|e| { - format!( - "failed to create directory at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -#[non_exhaustive] -pub struct DirEntry { - pub name: Option, - pub is_directory: bool, - pub is_file: bool, - pub is_symlink: bool, -} - -fn read_dir_inner>(path: P) -> crate::Result> { - let mut files_and_dirs: Vec = vec![]; - for entry in std::fs::read_dir(path)? { - let path = entry?.path(); - let file_type = path.metadata()?.file_type(); - files_and_dirs.push(DirEntry { - is_directory: file_type.is_dir(), - is_file: file_type.is_file(), - is_symlink: std::fs::symlink_metadata(&path) - .map(|md| md.file_type().is_symlink()) - .unwrap_or(false), - name: path - .file_name() - .map(|name| name.to_string_lossy()) - .map(|name| name.to_string()), - }); - } - Result::Ok(files_and_dirs) -} - -#[tauri::command] -pub async fn read_dir( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult> { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - - read_dir_inner(&resolved_path) - .map_err(|e| { - format!( - "failed to read directory at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub async fn read( - webview: Webview, - rid: ResourceId, - len: u32, -) -> CommandResult<(Vec, usize)> { - let mut data = vec![0; len as usize]; - let file = webview.resources_table().get::(rid)?; - let nread = StdFileResource::with_lock(&file, |mut file| file.read(&mut data)) - .map_err(|e| format!("faied to read bytes from file with error: {e}"))?; - Ok((data, nread)) -} - -#[tauri::command] -pub async fn read_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let (mut file, path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - OpenOptions { - base: BaseOptions { - base_dir: options.as_ref().and_then(|o| o.base_dir), - }, - options: crate::OpenOptions { - read: true, - ..Default::default() - }, - }, - )?; - - 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] -pub async fn read_text_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let (mut file, path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - OpenOptions { - base: BaseOptions { - base_dir: options.as_ref().and_then(|o| o.base_dir), - }, - options: crate::OpenOptions { - read: true, - ..Default::default() - }, - }, - )?; - - 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] -pub fn read_text_file_lines( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - use std::io::BufRead; - - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - - let file = File::open(&resolved_path).map_err(|e| { - format!( - "failed to open file at path: {} with error: {e}", - resolved_path.display() - ) - })?; - - let lines = BufReader::new(file).lines(); - let rid = webview.resources_table().add(StdLinesResource::new(lines)); - - Ok(rid) -} - -#[tauri::command] -pub async fn read_text_file_lines_next( - webview: Webview, - rid: ResourceId, -) -> CommandResult<(Option, bool)> { - let mut resource_table = webview.resources_table(); - let lines = resource_table.get::(rid)?; - - let ret = StdLinesResource::with_lock(&lines, |lines| { - lines.next().map(|a| (a.ok(), false)).unwrap_or_else(|| { - let _ = resource_table.close(rid); - (None, true) - }) - }); - - Ok(ret) -} - -#[derive(Debug, Clone, Deserialize)] -pub struct RemoveOptions { - #[serde(flatten)] - base: BaseOptions, - recursive: Option, -} - -#[tauri::command] -pub fn remove( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult<()> { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base.base_dir), - )?; - - let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| { - format!( - "failed to get metadata of path: {} with error: {e}", - resolved_path.display() - ) - })?; - - let file_type = metadata.file_type(); - - // taken from deno source code: https://github.com/denoland/deno/blob/429759fe8b4207240709c240a8344d12a1e39566/runtime/ops/fs.rs#L728 - let res = if file_type.is_file() { - std::fs::remove_file(&resolved_path) - } else if options.as_ref().and_then(|o| o.recursive).unwrap_or(false) { - std::fs::remove_dir_all(&resolved_path) - } else if file_type.is_symlink() { - #[cfg(unix)] - { - std::fs::remove_file(&resolved_path) - } - #[cfg(not(unix))] - { - use std::os::windows::fs::MetadataExt; - const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x00000010; - if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { - std::fs::remove_dir(&resolved_path) - } else { - std::fs::remove_file(&resolved_path) - } - } - } else if file_type.is_dir() { - std::fs::remove_dir(&resolved_path) - } else { - // pipes, sockets, etc... - std::fs::remove_file(&resolved_path) - }; - - res.map_err(|e| { - format!( - "failed to remove path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RenameOptions { - new_path_base_dir: Option, - old_path_base_dir: Option, -} - -#[tauri::command] -pub fn rename( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - old_path: SafeFilePath, - new_path: SafeFilePath, - options: Option, -) -> CommandResult<()> { - let resolved_old_path = resolve_path( - &webview, - &global_scope, - &command_scope, - old_path, - options.as_ref().and_then(|o| o.old_path_base_dir), - )?; - let resolved_new_path = resolve_path( - &webview, - &global_scope, - &command_scope, - new_path, - options.as_ref().and_then(|o| o.new_path_base_dir), - )?; - std::fs::rename(&resolved_old_path, &resolved_new_path) - .map_err(|e| { - format!( - "failed to rename old path: {} to new path: {} with error: {e}", - resolved_old_path.display(), - resolved_new_path.display() - ) - }) - .map_err(Into::into) -} - -#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] -#[repr(u16)] -pub enum SeekMode { - Start = 0, - Current = 1, - End = 2, -} - -#[tauri::command] -pub async fn seek( - webview: Webview, - rid: ResourceId, - offset: i64, - whence: SeekMode, -) -> CommandResult { - use std::io::{Seek, SeekFrom}; - let file = webview.resources_table().get::(rid)?; - StdFileResource::with_lock(&file, |mut file| { - file.seek(match whence { - SeekMode::Start => SeekFrom::Start(offset as u64), - SeekMode::Current => SeekFrom::Current(offset), - SeekMode::End => SeekFrom::End(offset), - }) - }) - .map_err(|e| format!("failed to seek file with error: {e}")) - .map_err(Into::into) -} - -#[cfg(target_os = "android")] -fn get_metadata std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - 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 std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - get_fs_metadata( - metadata_fn, - webview, - global_scope, - command_scope, - path, - options, - ) -} - -fn get_fs_metadata std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - webview, - global_scope, - command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - 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( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let metadata = get_metadata( - |p| std::fs::metadata(p), - &webview, - &global_scope, - &command_scope, - path, - options, - )?; - - Ok(get_stat(metadata)) -} - -#[tauri::command] -pub fn lstat( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let metadata = get_metadata( - |p| std::fs::symlink_metadata(p), - &webview, - &global_scope, - &command_scope, - path, - options, - )?; - Ok(get_stat(metadata)) -} - -#[tauri::command] -pub fn fstat(webview: Webview, rid: ResourceId) -> CommandResult { - let file = webview.resources_table().get::(rid)?; - let metadata = StdFileResource::with_lock(&file, |file| file.metadata()) - .map_err(|e| format!("failed to get metadata of file with error: {e}"))?; - Ok(get_stat(metadata)) -} - -#[tauri::command] -pub async fn truncate( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - len: Option, - options: Option, -) -> CommandResult<()> { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - let f = std::fs::OpenOptions::new() - .write(true) - .open(&resolved_path) - .map_err(|e| { - format!( - "failed to open file at path: {} with error: {e}", - resolved_path.display() - ) - })?; - f.set_len(len.unwrap_or(0)) - .map_err(|e| { - format!( - "failed to truncate file at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub async fn ftruncate( - webview: Webview, - rid: ResourceId, - len: Option, -) -> CommandResult<()> { - let file = webview.resources_table().get::(rid)?; - StdFileResource::with_lock(&file, |file| file.set_len(len.unwrap_or(0))) - .map_err(|e| format!("failed to truncate file with error: {e}")) - .map_err(Into::into) -} - -#[tauri::command] -pub async fn write( - webview: Webview, - rid: ResourceId, - data: Vec, -) -> CommandResult { - let file = webview.resources_table().get::(rid)?; - StdFileResource::with_lock(&file, |mut file| file.write(&data)) - .map_err(|e| format!("failed to write bytes to file with error: {e}")) - .map_err(Into::into) -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WriteFileOptions { - #[serde(flatten)] - base: BaseOptions, - #[serde(default)] - append: bool, - #[serde(default = "default_create_value")] - create: bool, - #[serde(default)] - create_new: bool, - #[allow(unused)] - mode: Option, -} - -fn default_create_value() -> bool { - true -} - -fn write_file_inner( - webview: Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - data: &[u8], - options: Option, -) -> CommandResult<()> { - let (mut file, path) = resolve_file( - &webview, - global_scope, - command_scope, - path, - 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, - }, - } - } 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}", - path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub async fn write_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - request: tauri::ipc::Request<'_>, -) -> CommandResult<()> { - let data = match request.body() { - tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data), - tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned( - data.iter() - .flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8))) - .collect(), - ), - _ => return Err(anyhow::anyhow!("unexpected invoke body").into()), - }; - - let path = request - .headers() - .get("path") - .ok_or_else(|| anyhow::anyhow!("missing file path").into()) - .and_then(|p| { - p.to_str() - .map_err(|e| anyhow::anyhow!("invalid path: {e}").into()) - }) - .and_then(|p| SafeFilePath::from_str(p).map_err(CommandError::from))?; - let options = request - .headers() - .get("options") - .and_then(|p| p.to_str().ok()) - .and_then(|opts| serde_json::from_str(opts).ok()); - write_file_inner(webview, &global_scope, &command_scope, path, &data, options) -} - -#[tauri::command] -pub async fn write_text_file( - #[allow(unused)] app: AppHandle, - #[allow(unused)] webview: Webview, - #[allow(unused)] global_scope: GlobalScope, - #[allow(unused)] command_scope: CommandScope, - path: SafeFilePath, - data: String, - #[allow(unused)] options: Option, -) -> CommandResult<()> { - write_file_inner( - webview, - &global_scope, - &command_scope, - path, - data.as_bytes(), - options, - ) -} - -#[tauri::command] -pub fn exists( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - Ok(resolved_path.exists()) -} - -#[cfg(not(target_os = "android"))] -pub fn resolve_file( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - 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( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - 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( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - 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( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - base_dir: Option, -) -> CommandResult { - let path = path.into_path()?; - let path = if let Some(base_dir) = base_dir { - webview.path().resolve(&path, base_dir)? - } else { - path - }; - - let scope = tauri::scope::fs::Scope::new( - webview, - &FsScope::Scope { - allow: webview - .fs_scope() - .allowed - .lock() - .unwrap() - .clone() - .into_iter() - .chain(global_scope.allows().iter().map(|e| e.path.clone())) - .chain(command_scope.allows().iter().map(|e| e.path.clone())) - .collect(), - deny: webview - .fs_scope() - .denied - .lock() - .unwrap() - .clone() - .into_iter() - .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: webview.fs_scope().require_literal_leading_dot, - }, - )?; - - if scope.is_allowed(&path) { - Ok(path) - } else { - Err(CommandError::Plugin(Error::PathForbidden(path))) - } -} - -struct StdFileResource(Mutex); - -impl StdFileResource { - fn new(file: File) -> Self { - Self(Mutex::new(file)) - } - - fn with_lock R>(&self, mut f: F) -> R { - let file = self.0.lock().unwrap(); - f(&file) - } -} - -impl Resource for StdFileResource {} - -struct StdLinesResource(Mutex>>); - -impl StdLinesResource { - fn new(lines: Lines>) -> Self { - Self(Mutex::new(lines)) - } - - fn with_lock>) -> R>(&self, mut f: F) -> R { - let mut lines = self.0.lock().unwrap(); - f(&mut lines) - } -} - -impl Resource for StdLinesResource {} - -// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913 -#[inline] -fn to_msec(maybe_time: std::result::Result) -> Option { - match maybe_time { - Ok(time) => { - let msec = time - .duration_since(UNIX_EPOCH) - .map(|t| t.as_millis() as u64) - .unwrap_or_else(|err| err.duration().as_millis() as u64); - Some(msec) - } - Err(_) => None, - } -} - -// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L926 -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct FileInfo { - is_file: bool, - is_directory: bool, - is_symlink: bool, - size: u64, - // In milliseconds, like JavaScript. Available on both Unix or Windows. - mtime: Option, - atime: Option, - birthtime: Option, - readonly: bool, - // Following are only valid under Windows. - file_attribues: Option, - // Following are only valid under Unix. - dev: Option, - ino: Option, - mode: Option, - nlink: Option, - uid: Option, - gid: Option, - rdev: Option, - blksize: Option, - blocks: Option, -} - -// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L950 -#[inline(always)] -fn get_stat(metadata: std::fs::Metadata) -> FileInfo { - // Unix stat member (number types only). 0 if not on unix. - macro_rules! usm { - ($member:ident) => {{ - #[cfg(unix)] - { - Some(metadata.$member()) - } - #[cfg(not(unix))] - { - None - } - }}; - } - - #[cfg(unix)] - use std::os::unix::fs::MetadataExt; - #[cfg(windows)] - use std::os::windows::fs::MetadataExt; - FileInfo { - is_file: metadata.is_file(), - is_directory: metadata.is_dir(), - is_symlink: metadata.file_type().is_symlink(), - size: metadata.len(), - // In milliseconds, like JavaScript. Available on both Unix or Windows. - mtime: to_msec(metadata.modified()), - atime: to_msec(metadata.accessed()), - birthtime: to_msec(metadata.created()), - readonly: metadata.permissions().readonly(), - // Following are only valid under Windows. - #[cfg(windows)] - file_attribues: Some(metadata.file_attributes()), - #[cfg(not(windows))] - file_attribues: None, - // Following are only valid under Unix. - dev: usm!(dev), - ino: usm!(ino), - mode: usm!(mode), - nlink: usm!(nlink), - uid: usm!(uid), - gid: usm!(gid), - rdev: usm!(rdev), - blksize: usm!(blksize), - blocks: usm!(blocks), - } -} - -mod test { - #[test] - fn safe_file_path_parse() { - use super::SafeFilePath; - - assert!(matches!( - serde_json::from_str::("\"C:/Users\""), - Ok(SafeFilePath::Path(_)) - )); - assert!(matches!( - serde_json::from_str::("\"file:///C:/Users\""), - Ok(SafeFilePath::Url(_)) - )); - } -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index db3bae4..0000000 --- a/src/config.rs +++ /dev/null @@ -1,19 +0,0 @@ -// 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, -} diff --git a/src/desktop.rs b/src/desktop.rs deleted file mode 100644 index 477c053..0000000 --- a/src/desktop.rs +++ /dev/null @@ -1,35 +0,0 @@ -// 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(pub(crate) AppHandle); - -fn path_or_err>(p: P) -> std::io::Result { - 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 Fs { - pub fn open>( - &self, - path: P, - opts: OpenOptions, - ) -> std::io::Result { - let path = path_or_err(path)?; - std::fs::OpenOptions::from(opts).open(path) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 0c98e83..0000000 --- a/src/error.rs +++ /dev/null @@ -1,43 +0,0 @@ -// 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}; - -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum Error { - #[error(transparent)] - Json(#[from] serde_json::Error), - #[error(transparent)] - Tauri(#[from] tauri::Error), - #[error(transparent)] - Io(#[from] std::io::Error), - #[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), - #[cfg(target_os = "android")] - #[error(transparent)] - PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), - #[error("URL is not a valid path")] - InvalidPathUrl, - #[error("Unsafe PathBuf: {0}")] - UnsafePathBuf(&'static str), -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} diff --git a/src/file_path.rs b/src/file_path.rs deleted file mode 100644 index 967696b..0000000 --- a/src/file_path.rs +++ /dev/null @@ -1,314 +0,0 @@ -// 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 contaiend [`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 { - match self { - Self::Url(url) => url - .to_file_path() - .map(PathBuf::from) - .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 contaiend [`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 { - match self { - Self::Url(url) => url - .to_file_path() - .map(PathBuf::from) - .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(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct FilePathVisitor; - - impl<'de> serde::de::Visitor<'de> 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(self, s: &str) -> std::result::Result - 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(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct SafeFilePathVisitor; - - impl<'de> serde::de::Visitor<'de> 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(self, s: &str) -> std::result::Result - 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 { - 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 { - 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 for FilePath { - fn from(value: PathBuf) -> Self { - Self::Path(value) - } -} - -impl TryFrom for SafeFilePath { - type Error = Error; - fn try_from(value: PathBuf) -> Result { - 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 { - 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 { - SafePathBuf::new(value.to_owned()) - .map(SafeFilePath::Path) - .map_err(Error::UnsafePathBuf) - } -} - -impl From for FilePath { - fn from(value: url::Url) -> Self { - Self::Url(value) - } -} - -impl From for SafeFilePath { - fn from(value: url::Url) -> Self { - Self::Url(value) - } -} - -impl TryFrom for PathBuf { - type Error = Error; - fn try_from(value: FilePath) -> Result { - value.into_path() - } -} - -impl TryFrom for PathBuf { - type Error = Error; - fn try_from(value: SafeFilePath) -> Result { - value.into_path() - } -} - -impl From 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 for SafeFilePath { - type Error = Error; - - fn try_from(value: FilePath) -> Result { - match value { - FilePath::Url(url) => Ok(SafeFilePath::Url(url)), - FilePath::Path(p) => SafePathBuf::new(p) - .map(SafeFilePath::Path) - .map_err(Error::UnsafePathBuf), - } - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 5cb903f..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,459 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/fs/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/fs) -//! -//! Access the file system. - -#![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; - -use serde::Deserialize; -use tauri::{ - ipc::ScopeObject, - plugin::{Builder as PluginBuilder, TauriPlugin}, - utils::acl::Value, - AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, -}; - -mod commands; -mod config; -#[cfg(not(target_os = "android"))] -mod desktop; -mod error; -mod file_path; -#[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}; - -pub use file_path::FilePath; -pub use file_path::SafeFilePath; - -type Result = std::result::Result; - -#[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, - #[serde(default)] - #[allow(unused)] - custom_flags: Option, -} - -fn default_true() -> bool { - true -} - -impl From 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 [seek]\([SeekFrom]::[End]\(0)) 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 - /// [Seek]::[stream_position]), 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 Fs { - pub fn read_to_string>(&self, path: P) -> std::io::Result { - let mut s = String::new(); - self.open( - path, - OpenOptions { - read: true, - ..Default::default() - }, - )? - .read_to_string(&mut s)?; - Ok(s) - } - - pub fn read>(&self, path: P) -> std::io::Result> { - 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( - app: &AppHandle, - raw: Value, - ) -> std::result::Result { - let entry = serde_json::from_value(raw.into()).map(|raw| { - let path = match raw { - scope::EntryRaw::Value(path) => path, - scope::EntryRaw::Object { path } => path, - }; - Self { path } - })?; - - Ok(Self { - path: app.path().parse(entry.path)?, - }) - } -} - -pub trait FsExt { - 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; -} - -impl> FsExt for T { - fn fs_scope(&self) -> &Scope { - self.state::().inner() - } - - fn try_fs_scope(&self) -> Option<&Scope> { - self.try_state::().map(|s| s.inner()) - } - - fn fs(&self) -> &Fs { - self.state::>().inner() - } -} - -pub fn init() -> TauriPlugin> { - PluginBuilder::>::new("fs") - .invoke_handler(tauri::generate_handler![ - commands::create, - commands::open, - commands::copy_file, - commands::close, - 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, - #[cfg(feature = "watch")] - watcher::watch, - #[cfg(feature = "watch")] - watcher::unwatch - ]) - .setup(|app, api| { - let mut scope = Scope::default(); - scope.require_literal_leading_dot = api - .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(()) - }) - .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() { - scope.allow_file(path); - } else { - scope.allow_directory(path, true); - } - } - } - }) - .build() -} diff --git a/src/mobile.rs b/src/mobile.rs deleted file mode 100644 index 06422be..0000000 --- a/src/mobile.rs +++ /dev/null @@ -1,96 +0,0 @@ -// 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( - _app: &AppHandle, - api: PluginApi, -) -> crate::Result> { - #[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(PluginHandle); - -impl Fs { - pub fn open>( - &self, - path: P, - opts: OpenOptions, - ) -> std::io::Result { - 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, - mode: impl Into, - ) -> crate::Result { - #[cfg(target_os = "android")] - { - let result = self.0.run_mobile_plugin::( - "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!() - } - } - } -} diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index b9edc2c..0000000 --- a/src/models.rs +++ /dev/null @@ -1,18 +0,0 @@ -// 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, -} diff --git a/src/scope.rs b/src/scope.rs deleted file mode 100644 index f31c786..0000000 --- a/src/scope.rs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicU32, Ordering}, - Mutex, - }, -}; - -use serde::Deserialize; - -#[doc(hidden)] -#[derive(Deserialize)] -#[serde(untagged)] -pub enum EntryRaw { - Value(PathBuf), - Object { path: PathBuf }, -} - -#[derive(Debug)] -pub struct Entry { - pub path: PathBuf, -} - -pub type EventId = u32; -type EventListener = Box; - -/// Scope change event. -#[derive(Debug, Clone)] -pub enum Event { - /// A path has been allowed. - PathAllowed(PathBuf), - /// A path has been forbidden. - PathForbidden(PathBuf), -} - -#[derive(Default)] -pub struct Scope { - pub(crate) allowed: Mutex>, - pub(crate) denied: Mutex>, - event_listeners: Mutex>, - next_event_id: AtomicU32, - pub(crate) require_literal_leading_dot: Option, -} - -impl Scope { - /// Extend the allowed patterns with the given directory. - /// - /// After this function has been called, the frontend will be able to use the Tauri API to read - /// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too. - pub fn allow_directory>(&self, path: P, recursive: bool) { - let path = path.as_ref(); - - { - let mut allowed = self.allowed.lock().unwrap(); - allowed.push(path.to_path_buf()); - allowed.push(path.join(if recursive { "**" } else { "*" })); - } - - self.emit(Event::PathAllowed(path.to_path_buf())); - } - - /// Extend the allowed patterns with the given file path. - /// - /// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file. - pub fn allow_file>(&self, path: P) { - let path = path.as_ref(); - - self.allowed.lock().unwrap().push(path.to_path_buf()); - - self.emit(Event::PathAllowed(path.to_path_buf())); - } - - /// Set the given directory path to be forbidden by this scope. - /// - /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. - pub fn forbid_directory>(&self, path: P, recursive: bool) { - let path = path.as_ref(); - - { - let mut denied = self.denied.lock().unwrap(); - denied.push(path.to_path_buf()); - denied.push(path.join(if recursive { "**" } else { "*" })); - } - - self.emit(Event::PathForbidden(path.to_path_buf())); - } - - /// Set the given file path to be forbidden by this scope. - /// - /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. - pub fn forbid_file>(&self, path: P) { - let path = path.as_ref(); - - self.denied.lock().unwrap().push(path.to_path_buf()); - - self.emit(Event::PathForbidden(path.to_path_buf())); - } - - /// List of allowed paths. - pub fn allowed(&self) -> Vec { - self.allowed.lock().unwrap().clone() - } - - /// List of forbidden paths. - pub fn forbidden(&self) -> Vec { - self.denied.lock().unwrap().clone() - } - - fn next_event_id(&self) -> u32 { - self.next_event_id.fetch_add(1, Ordering::Relaxed) - } - - fn emit(&self, event: Event) { - let listeners = self.event_listeners.lock().unwrap(); - let handlers = listeners.values(); - for listener in handlers { - listener(&event); - } - } - - /// Listen to an event on this scope. - pub fn listen(&self, f: F) -> EventId { - let id = self.next_event_id(); - self.event_listeners.lock().unwrap().insert(id, Box::new(f)); - id - } -} diff --git a/src/watcher.rs b/src/watcher.rs deleted file mode 100644 index cf2af50..0000000 --- a/src/watcher.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; -use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileIdMap}; -use serde::Deserialize; -use tauri::{ - ipc::{Channel, CommandScope, GlobalScope}, - path::BaseDirectory, - Manager, Resource, ResourceId, Runtime, Webview, -}; - -use std::{ - path::PathBuf, - sync::{ - mpsc::{channel, Receiver}, - Mutex, - }, - thread::spawn, - time::Duration, -}; - -use crate::{ - commands::{resolve_path, CommandResult}, - scope::Entry, - SafeFilePath, -}; - -struct InnerWatcher { - pub kind: WatcherKind, - paths: Vec, -} - -pub struct WatcherResource(Mutex); -impl WatcherResource { - fn new(kind: WatcherKind, paths: Vec) -> Self { - Self(Mutex::new(InnerWatcher { kind, paths })) - } - - fn with_lock R>(&self, mut f: F) -> R { - let mut watcher = self.0.lock().unwrap(); - f(&mut watcher) - } -} - -impl Resource for WatcherResource {} - -enum WatcherKind { - Debouncer(Debouncer), - Watcher(RecommendedWatcher), -} - -fn watch_raw(on_event: Channel, rx: Receiver>) { - spawn(move || { - while let Ok(event) = rx.recv() { - if let Ok(event) = event { - // TODO: Should errors be emitted too? - let _ = on_event.send(event); - } - } - }); -} - -fn watch_debounced(on_event: Channel, rx: Receiver) { - spawn(move || { - while let Ok(Ok(events)) = rx.recv() { - for event in events { - // TODO: Should errors be emitted too? - let _ = on_event.send(event.event); - } - } - }); -} - -#[derive(Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WatchOptions { - base_dir: Option, - recursive: bool, - delay_ms: Option, -} - -#[tauri::command] -pub async fn watch( - webview: Webview, - paths: Vec, - options: WatchOptions, - on_event: Channel, - global_scope: GlobalScope, - command_scope: CommandScope, -) -> CommandResult { - let mut resolved_paths = Vec::with_capacity(paths.capacity()); - for path in paths { - resolved_paths.push(resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.base_dir, - )?); - } - - let recursive_mode = if options.recursive { - RecursiveMode::Recursive - } else { - RecursiveMode::NonRecursive - }; - - let kind = if let Some(delay) = options.delay_ms { - let (tx, rx) = channel(); - let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?; - for path in &resolved_paths { - debouncer.watcher().watch(path.as_ref(), recursive_mode)?; - debouncer.cache().add_root(path, recursive_mode); - } - watch_debounced(on_event, rx); - WatcherKind::Debouncer(debouncer) - } else { - let (tx, rx) = channel(); - let mut watcher = RecommendedWatcher::new(tx, Config::default())?; - for path in &resolved_paths { - watcher.watch(path.as_ref(), recursive_mode)?; - } - watch_raw(on_event, rx); - WatcherKind::Watcher(watcher) - }; - - let rid = webview - .resources_table() - .add(WatcherResource::new(kind, resolved_paths)); - - Ok(rid) -} - -#[tauri::command] -pub async fn unwatch(webview: Webview, rid: ResourceId) -> CommandResult<()> { - let watcher = webview.resources_table().take::(rid)?; - WatcherResource::with_lock(&watcher, |watcher| { - match &mut watcher.kind { - WatcherKind::Debouncer(ref mut debouncer) => { - for path in &watcher.paths { - debouncer.watcher().unwatch(path.as_ref()).map_err(|e| { - format!("failed to unwatch path: {} with error: {e}", path.display()) - })?; - } - } - WatcherKind::Watcher(ref mut w) => { - for path in &watcher.paths { - w.unwatch(path.as_ref()).map_err(|e| { - format!("failed to unwatch path: {} with error: {e}", path.display()) - })?; - } - } - } - - Ok(()) - }) -}