From f3c2639632ba498750eb0489961f6461caae0ca1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 02:27:43 +0000 Subject: [PATCH 1/7] feat: add fs2 plugin wrapping tauri-plugin-fs Co-Authored-By: yujonglee --- Cargo.lock | 16 + Cargo.toml | 1 + apps/desktop/package.json | 1 + apps/desktop/src-tauri/Cargo.toml | 1 + .../src-tauri/capabilities/default.json | 1 + plugins/fs2/.gitignore | 17 + plugins/fs2/Cargo.toml | 26 ++ plugins/fs2/build.rs | 5 + plugins/fs2/js/bindings.gen.ts | 77 +++++ plugins/fs2/js/index.ts | 1 + plugins/fs2/package.json | 11 + .../autogenerated/commands/ping.toml | 13 + .../permissions/autogenerated/reference.md | 43 +++ plugins/fs2/permissions/default.toml | 3 + plugins/fs2/permissions/schemas/schema.json | 318 ++++++++++++++++++ plugins/fs2/src/commands.rs | 7 + plugins/fs2/src/error.rs | 18 + plugins/fs2/src/ext.rs | 29 ++ plugins/fs2/src/lib.rs | 63 ++++ plugins/fs2/tsconfig.json | 5 + pnpm-lock.yaml | 9 + 21 files changed, 665 insertions(+) create mode 100644 plugins/fs2/.gitignore create mode 100644 plugins/fs2/Cargo.toml create mode 100644 plugins/fs2/build.rs create mode 100644 plugins/fs2/js/bindings.gen.ts create mode 100644 plugins/fs2/js/index.ts create mode 100644 plugins/fs2/package.json create mode 100644 plugins/fs2/permissions/autogenerated/commands/ping.toml create mode 100644 plugins/fs2/permissions/autogenerated/reference.md create mode 100644 plugins/fs2/permissions/default.toml create mode 100644 plugins/fs2/permissions/schemas/schema.json create mode 100644 plugins/fs2/src/commands.rs create mode 100644 plugins/fs2/src/error.rs create mode 100644 plugins/fs2/src/ext.rs create mode 100644 plugins/fs2/src/lib.rs create mode 100644 plugins/fs2/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 3daa9a83ec..6f247c2109 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4733,6 +4733,7 @@ dependencies = [ "tauri-plugin-extensions", "tauri-plugin-fs", "tauri-plugin-fs-sync", + "tauri-plugin-fs2", "tauri-plugin-hooks", "tauri-plugin-http", "tauri-plugin-icon", @@ -18301,6 +18302,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "tauri-plugin-fs2" +version = "0.1.0" +dependencies = [ + "serde", + "specta", + "specta-typescript", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "tauri-specta", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "tauri-plugin-hooks" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 79bd65f225..a7fc0f7aed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,7 @@ tauri-plugin-deeplink2 = { path = "plugins/deeplink2" } tauri-plugin-detect = { path = "plugins/detect" } tauri-plugin-extensions = { path = "plugins/extensions" } tauri-plugin-fs-sync = { path = "plugins/fs-sync" } +tauri-plugin-fs2 = { path = "plugins/fs2" } tauri-plugin-hooks = { path = "plugins/hooks" } tauri-plugin-icon = { path = "plugins/icon" } tauri-plugin-importer = { path = "plugins/importer" } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index af6ae84961..dc341909fa 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -46,6 +46,7 @@ "@hypr/plugin-detect": "workspace:*", "@hypr/plugin-extensions": "workspace:*", "@hypr/plugin-fs-sync": "workspace:*", + "@hypr/plugin-fs2": "workspace:*", "@hypr/plugin-hooks": "workspace:*", "@hypr/plugin-icon": "workspace:*", "@hypr/plugin-importer": "workspace:*", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index ed01d4dde9..3cd784d998 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -38,6 +38,7 @@ tauri-plugin-dialog = { workspace = true } tauri-plugin-extensions = { workspace = true } tauri-plugin-fs = { workspace = true } tauri-plugin-fs-sync = { workspace = true } +tauri-plugin-fs2 = { workspace = true } tauri-plugin-hooks = { workspace = true } tauri-plugin-http = { workspace = true } tauri-plugin-icon = { workspace = true } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index f894789167..899ee230a4 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -258,6 +258,7 @@ }, "misc:default", "fs-sync:default", + "fs2:default", "os:default", "detect:default", "permissions:default", diff --git a/plugins/fs2/.gitignore b/plugins/fs2/.gitignore new file mode 100644 index 0000000000..50d8e32e89 --- /dev/null +++ b/plugins/fs2/.gitignore @@ -0,0 +1,17 @@ +/.vs +.DS_Store +.Thumbs.db +*.sublime* +.idea/ +debug.log +package-lock.json +.vscode/settings.json +yarn.lock + +/.tauri +/target +Cargo.lock +node_modules/ + +dist-js +dist diff --git a/plugins/fs2/Cargo.toml b/plugins/fs2/Cargo.toml new file mode 100644 index 0000000000..f3e128991d --- /dev/null +++ b/plugins/fs2/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tauri-plugin-fs2" +version = "0.1.0" +authors = ["You"] +edition = "2024" +exclude = ["/js", "/node_modules"] +links = "tauri-plugin-fs2" +description = "" + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +specta-typescript = { workspace = true } +tokio = { workspace = true, features = ["macros"] } + +[dependencies] +tauri-plugin-fs = { workspace = true } + +tauri = { workspace = true, features = ["test"] } +tauri-specta = { workspace = true, features = ["derive", "typescript"] } + +serde = { workspace = true } +specta = { workspace = true } + +thiserror = { workspace = true } diff --git a/plugins/fs2/build.rs b/plugins/fs2/build.rs new file mode 100644 index 0000000000..029861396b --- /dev/null +++ b/plugins/fs2/build.rs @@ -0,0 +1,5 @@ +const COMMANDS: &[&str] = &["ping"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS).build(); +} diff --git a/plugins/fs2/js/bindings.gen.ts b/plugins/fs2/js/bindings.gen.ts new file mode 100644 index 0000000000..987c7df2f5 --- /dev/null +++ b/plugins/fs2/js/bindings.gen.ts @@ -0,0 +1,77 @@ +// @ts-nocheck +/** user-defined events **/ +/** user-defined constants **/ +/** user-defined types **/ +/** tauri-specta globals **/ +import { + Channel as TAURI_CHANNEL, + invoke as TAURI_INVOKE, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + +export const commands = { + async ping(): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:fs2|ping") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, +}; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/plugins/fs2/js/index.ts b/plugins/fs2/js/index.ts new file mode 100644 index 0000000000..a96e122f03 --- /dev/null +++ b/plugins/fs2/js/index.ts @@ -0,0 +1 @@ +export * from "./bindings.gen"; diff --git a/plugins/fs2/package.json b/plugins/fs2/package.json new file mode 100644 index 0000000000..cac2e6012d --- /dev/null +++ b/plugins/fs2/package.json @@ -0,0 +1,11 @@ +{ + "name": "@hypr/plugin-fs2", + "private": true, + "main": "./js/index.ts", + "scripts": { + "codegen": "cargo test -p tauri-plugin-fs2" + }, + "dependencies": { + "@tauri-apps/api": "^2.9.1" + } +} diff --git a/plugins/fs2/permissions/autogenerated/commands/ping.toml b/plugins/fs2/permissions/autogenerated/commands/ping.toml new file mode 100644 index 0000000000..1d1358807e --- /dev/null +++ b/plugins/fs2/permissions/autogenerated/commands/ping.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-ping" +description = "Enables the ping command without any pre-configured scope." +commands.allow = ["ping"] + +[[permission]] +identifier = "deny-ping" +description = "Denies the ping command without any pre-configured scope." +commands.deny = ["ping"] diff --git a/plugins/fs2/permissions/autogenerated/reference.md b/plugins/fs2/permissions/autogenerated/reference.md new file mode 100644 index 0000000000..69e2153b30 --- /dev/null +++ b/plugins/fs2/permissions/autogenerated/reference.md @@ -0,0 +1,43 @@ +## Default Permission + +Default permissions for the plugin + +#### This default permission set includes the following: + +- `allow-ping` + +## Permission Table + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`fs2:allow-ping` + + + +Enables the ping command without any pre-configured scope. + +
+ +`fs2:deny-ping` + + + +Denies the ping command without any pre-configured scope. + +
diff --git a/plugins/fs2/permissions/default.toml b/plugins/fs2/permissions/default.toml new file mode 100644 index 0000000000..cc5a76f22e --- /dev/null +++ b/plugins/fs2/permissions/default.toml @@ -0,0 +1,3 @@ +[default] +description = "Default permissions for the plugin" +permissions = ["allow-ping"] diff --git a/plugins/fs2/permissions/schemas/schema.json b/plugins/fs2/permissions/schemas/schema.json new file mode 100644 index 0000000000..ac68e129e2 --- /dev/null +++ b/plugins/fs2/permissions/schemas/schema.json @@ -0,0 +1,318 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the ping command without any pre-configured scope.", + "type": "string", + "const": "allow-ping", + "markdownDescription": "Enables the ping command without any pre-configured scope." + }, + { + "description": "Denies the ping command without any pre-configured scope.", + "type": "string", + "const": "deny-ping", + "markdownDescription": "Denies the ping command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`", + "type": "string", + "const": "default", + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/fs2/src/commands.rs b/plugins/fs2/src/commands.rs new file mode 100644 index 0000000000..d36009dbbb --- /dev/null +++ b/plugins/fs2/src/commands.rs @@ -0,0 +1,7 @@ +use crate::Fs2PluginExt; + +#[tauri::command] +#[specta::specta] +pub(crate) async fn ping(app: tauri::AppHandle) -> Result { + app.fs2().ping().map_err(|e| e.to_string()) +} diff --git a/plugins/fs2/src/error.rs b/plugins/fs2/src/error.rs new file mode 100644 index 0000000000..53206e2ee5 --- /dev/null +++ b/plugins/fs2/src/error.rs @@ -0,0 +1,18 @@ +use serde::{Serialize, ser::Serializer}; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), +} + +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/plugins/fs2/src/ext.rs b/plugins/fs2/src/ext.rs new file mode 100644 index 0000000000..3694a8bd68 --- /dev/null +++ b/plugins/fs2/src/ext.rs @@ -0,0 +1,29 @@ +pub struct Fs2<'a, R: tauri::Runtime, M: tauri::Manager> { + #[allow(dead_code)] + manager: &'a M, + _runtime: std::marker::PhantomData R>, +} + +impl<'a, R: tauri::Runtime, M: tauri::Manager> Fs2<'a, R, M> { + pub fn ping(&self) -> Result { + Ok("pong".to_string()) + } +} + +pub trait Fs2PluginExt { + fn fs2(&self) -> Fs2<'_, R, Self> + where + Self: tauri::Manager + Sized; +} + +impl> Fs2PluginExt for T { + fn fs2(&self) -> Fs2<'_, R, Self> + where + Self: Sized, + { + Fs2 { + manager: self, + _runtime: std::marker::PhantomData, + } + } +} diff --git a/plugins/fs2/src/lib.rs b/plugins/fs2/src/lib.rs new file mode 100644 index 0000000000..4ddb0f08a4 --- /dev/null +++ b/plugins/fs2/src/lib.rs @@ -0,0 +1,63 @@ +mod commands; +mod error; +mod ext; + +pub use error::{Error, Result}; +pub use ext::*; + +const PLUGIN_NAME: &str = "fs2"; + +fn make_specta_builder() -> tauri_specta::Builder { + tauri_specta::Builder::::new() + .plugin_name(PLUGIN_NAME) + .commands(tauri_specta::collect_commands![ + commands::ping::, + ]) + .error_handling(tauri_specta::ErrorHandlingMode::Result) +} + +pub fn init() -> tauri::plugin::TauriPlugin { + let specta_builder = make_specta_builder(); + + tauri::plugin::Builder::new(PLUGIN_NAME) + .invoke_handler(specta_builder.invoke_handler()) + .build() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn export_types() { + const OUTPUT_FILE: &str = "./js/bindings.gen.ts"; + + make_specta_builder::() + .export( + specta_typescript::Typescript::default() + .formatter(specta_typescript::formatter::prettier) + .bigint(specta_typescript::BigIntExportBehavior::Number), + OUTPUT_FILE, + ) + .unwrap(); + + let content = std::fs::read_to_string(OUTPUT_FILE).unwrap(); + std::fs::write(OUTPUT_FILE, format!("// @ts-nocheck\n{content}")).unwrap(); + } + + fn create_app(builder: tauri::Builder) -> tauri::App { + let mut ctx = tauri::test::mock_context(tauri::test::noop_assets()); + ctx.config_mut().identifier = "com.hyprnote.dev".to_string(); + ctx.config_mut().version = Some("0.0.1".to_string()); + + builder.plugin(init()).build(ctx).unwrap() + } + + #[tokio::test] + async fn test_ping() { + let app = create_app(tauri::test::mock_builder()); + let result = app.fs2().ping(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "pong"); + } +} diff --git a/plugins/fs2/tsconfig.json b/plugins/fs2/tsconfig.json new file mode 100644 index 0000000000..13b985325d --- /dev/null +++ b/plugins/fs2/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["./js/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60e6f2f7bc..9bbf662262 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,6 +287,9 @@ importers: '@hypr/plugin-fs-sync': specifier: workspace:* version: link:../../plugins/fs-sync + '@hypr/plugin-fs2': + specifier: workspace:* + version: link:../../plugins/fs2 '@hypr/plugin-hooks': specifier: workspace:* version: link:../../plugins/hooks @@ -1544,6 +1547,12 @@ importers: specifier: ^2.9.1 version: 2.9.1 + plugins/fs2: + dependencies: + '@tauri-apps/api': + specifier: ^2.9.1 + version: 2.9.1 + plugins/hooks: dependencies: '@tauri-apps/api': From 9c466be87677284badb509f8d19e3e0eebf48392 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 02:29:41 +0000 Subject: [PATCH 2/7] fix: add fs2 plugin registration to lib.rs Co-Authored-By: yujonglee --- apps/desktop/src-tauri/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 0f7d6d929b..5c7fac732a 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -104,6 +104,7 @@ pub async fn main() { .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deeplink2::init()) .plugin(tauri_plugin_fs_sync::init()) + .plugin(tauri_plugin_fs2::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_path2::init()) From 98d76bb411a2aee8faa159d52b87a962163f3ae2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:40:11 +0000 Subject: [PATCH 3/7] feat: implement fs2 plugin with readTextFile and remove operations - Add readTextFile and remove commands to fs2 plugin - Implement path validation in Rust using path2's base directory - Replace all @tauri-apps/plugin-fs usages with fs2 plugin - Update test files to mock fs2 instead of @tauri-apps/plugin-fs - Remove @tauri-apps/plugin-fs dependency from package.json Co-Authored-By: yujonglee --- Cargo.lock | 2 +- apps/desktop/package.json | 1 - .../src/store/tinybase/persister/chat/load.ts | 25 ++- .../tinybase/persister/factories/collector.ts | 11 +- .../persister/factories/json-file.test.ts | 20 ++- .../tinybase/persister/factories/json-file.ts | 21 ++- .../persister/factories/markdown-dir.test.ts | 10 +- .../persister/factories/markdown-dir.ts | 38 ++--- .../factories/multi-table-dir.test.ts | 10 +- .../persister/human/persister.test.ts | 10 +- .../persister/organization/persister.test.ts | 10 +- .../src/store/tinybase/store/importer.test.ts | 109 +++++++++---- .../src/store/tinybase/store/importer.ts | 27 +++- plugins/fs2/Cargo.toml | 4 +- plugins/fs2/js/bindings.gen.ts | 146 +++++++++++------- plugins/fs2/src/commands.rs | 22 +++ plugins/fs2/src/error.rs | 4 + plugins/fs2/src/ext.rs | 72 ++++++++- plugins/fs2/src/lib.rs | 2 + pnpm-lock.yaml | 10 -- 20 files changed, 369 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f247c2109..6770c91ce8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18311,7 +18311,7 @@ dependencies = [ "specta-typescript", "tauri", "tauri-plugin", - "tauri-plugin-fs", + "tauri-plugin-path2", "tauri-specta", "thiserror 2.0.17", "tokio", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index dc341909fa..bfbd545200 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -91,7 +91,6 @@ "@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-deep-link": "^2.4.5", "@tauri-apps/plugin-dialog": "^2.4.2", - "@tauri-apps/plugin-fs": "^2.4.4", "@tauri-apps/plugin-http": "^2.5.4", "@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-os": "^2.3.2", diff --git a/apps/desktop/src/store/tinybase/persister/chat/load.ts b/apps/desktop/src/store/tinybase/persister/chat/load.ts index 420e281ecd..5cf373cc36 100644 --- a/apps/desktop/src/store/tinybase/persister/chat/load.ts +++ b/apps/desktop/src/store/tinybase/persister/chat/load.ts @@ -1,6 +1,6 @@ import { sep } from "@tauri-apps/api/path"; -import { readTextFile } from "@tauri-apps/plugin-fs"; +import { commands as fs2Commands } from "@hypr/plugin-fs2"; import { commands as fsSyncCommands } from "@hypr/plugin-fs-sync"; import { @@ -96,15 +96,26 @@ export async function loadSingleChatGroup( ): Promise> { const filePath = [dataDir, "chats", groupId, CHAT_MESSAGES_FILE].join(sep()); + const result = await fs2Commands.readTextFile(filePath); + if (result.status === "error") { + if (isFileNotFoundError(result.error)) { + return ok(createEmptyLoadedChatData()); + } + console.error( + `[${LABEL}] Failed to load chat group ${groupId}:`, + result.error, + ); + return err(result.error); + } + try { - const content = await readTextFile(filePath); - const json = JSON.parse(content) as ChatJson; + const json = JSON.parse(result.data) as ChatJson; return ok(chatJsonToData(json)); } catch (error) { - if (isFileNotFoundError(error)) { - return ok(createEmptyLoadedChatData()); - } - console.error(`[${LABEL}] Failed to load chat group ${groupId}:`, error); + console.error( + `[${LABEL}] Failed to parse chat JSON for ${groupId}:`, + error, + ); return err(String(error)); } } diff --git a/apps/desktop/src/store/tinybase/persister/factories/collector.ts b/apps/desktop/src/store/tinybase/persister/factories/collector.ts index 4433604da2..752f24d708 100644 --- a/apps/desktop/src/store/tinybase/persister/factories/collector.ts +++ b/apps/desktop/src/store/tinybase/persister/factories/collector.ts @@ -1,4 +1,3 @@ -import { remove } from "@tauri-apps/plugin-fs"; import { createCustomPersister } from "tinybase/persisters/with-schemas"; import type { PersistedChanges, @@ -11,6 +10,7 @@ import type { OptionalSchemas, } from "tinybase/with-schemas"; +import { commands as fs2Commands } from "@hypr/plugin-fs2"; import { commands as fsSyncCommands, type ParsedDocument, @@ -235,12 +235,11 @@ async function deleteFiles(paths: string[], label: string): Promise { if (paths.length === 0) return; for (const path of paths) { - try { - await remove(path); - } catch (error) { - const errorStr = String(error); + const result = await fs2Commands.remove(path); + if (result.status === "error") { + const errorStr = result.error; if (!errorStr.includes("No such file") && !errorStr.includes("ENOENT")) { - console.error(`[${label}] Failed to delete file ${path}:`, error); + console.error(`[${label}] Failed to delete file ${path}:`, errorStr); } } } diff --git a/apps/desktop/src/store/tinybase/persister/factories/json-file.test.ts b/apps/desktop/src/store/tinybase/persister/factories/json-file.test.ts index b86c810c98..308fa66d8f 100644 --- a/apps/desktop/src/store/tinybase/persister/factories/json-file.test.ts +++ b/apps/desktop/src/store/tinybase/persister/factories/json-file.test.ts @@ -7,9 +7,9 @@ const path2Mocks = vi.hoisted(() => ({ base: vi.fn().mockResolvedValue("/mock/data/dir/hyprnote"), })); -const fsMocks = vi.hoisted(() => ({ - mkdir: vi.fn().mockResolvedValue(undefined), +const fs2Mocks = vi.hoisted(() => ({ readTextFile: vi.fn(), + remove: vi.fn(), })); const fsSyncMocks = vi.hoisted(() => ({ @@ -23,7 +23,7 @@ const notifyMocks = vi.hoisted(() => ({ })); vi.mock("@hypr/plugin-path2", () => ({ commands: path2Mocks })); -vi.mock("@tauri-apps/plugin-fs", () => fsMocks); +vi.mock("@hypr/plugin-fs2", () => ({ commands: fs2Mocks })); vi.mock("@hypr/plugin-fs-sync", () => ({ commands: fsSyncMocks })); vi.mock("@hypr/plugin-notify", () => ({ events: notifyMocks })); @@ -67,7 +67,10 @@ describe("createJsonFilePersister", () => { recurrence_series_id: "", }, }; - fsMocks.readTextFile.mockResolvedValue(JSON.stringify(mockData)); + fs2Mocks.readTextFile.mockResolvedValue({ + status: "ok", + data: JSON.stringify(mockData), + }); const persister = createJsonFilePersister(store, { tableName: "events", @@ -76,16 +79,17 @@ describe("createJsonFilePersister", () => { }); await persister.load(); - expect(fsMocks.readTextFile).toHaveBeenCalledWith( + expect(fs2Mocks.readTextFile).toHaveBeenCalledWith( `${MOCK_DATA_DIR}/test.json`, ); expect(store.getTable("events")).toEqual(mockData); }); test("handles file not found gracefully", async () => { - fsMocks.readTextFile.mockRejectedValue( - new Error("No such file or directory"), - ); + fs2Mocks.readTextFile.mockResolvedValue({ + status: "error", + error: "No such file or directory", + }); const persister = createJsonFilePersister(store, { tableName: "events", diff --git a/apps/desktop/src/store/tinybase/persister/factories/json-file.ts b/apps/desktop/src/store/tinybase/persister/factories/json-file.ts index eb36b1fbbe..e56ecc8bcd 100644 --- a/apps/desktop/src/store/tinybase/persister/factories/json-file.ts +++ b/apps/desktop/src/store/tinybase/persister/factories/json-file.ts @@ -1,5 +1,4 @@ import { sep } from "@tauri-apps/api/path"; -import { readTextFile } from "@tauri-apps/plugin-fs"; import { createCustomPersister } from "tinybase/persisters/with-schemas"; import type { PersistedChanges, @@ -7,6 +6,7 @@ import type { } from "tinybase/persisters/with-schemas"; import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas"; +import { commands as fs2Commands } from "@hypr/plugin-fs2"; import { commands as fsSyncCommands, type JsonValue, @@ -159,14 +159,21 @@ async function loadTableData( filename: string, label: string, ): Promise> | undefined> { + const base = await path2Commands.base(); + const path = [base, filename].join(sep()); + const result = await fs2Commands.readTextFile(path); + + if (result.status === "error") { + if (!isFileNotFoundError(result.error)) { + console.error(`[${label}] load error:`, result.error); + } + return undefined; + } + try { - const base = await path2Commands.base(); - const content = await readTextFile([base, filename].join(sep())); - return JSON.parse(content); + return JSON.parse(result.data); } catch (error) { - if (!isFileNotFoundError(error)) { - console.error(`[${label}] load error:`, error); - } + console.error(`[${label}] JSON parse error:`, error); return undefined; } } diff --git a/apps/desktop/src/store/tinybase/persister/factories/markdown-dir.test.ts b/apps/desktop/src/store/tinybase/persister/factories/markdown-dir.test.ts index c23b80c523..9d4564b744 100644 --- a/apps/desktop/src/store/tinybase/persister/factories/markdown-dir.test.ts +++ b/apps/desktop/src/store/tinybase/persister/factories/markdown-dir.test.ts @@ -22,18 +22,14 @@ const fsSyncMocks = vi.hoisted(() => ({ cleanupOrphan: vi.fn().mockResolvedValue({ status: "ok", data: 0 }), })); -const fsMocks = vi.hoisted(() => ({ - mkdir: vi.fn().mockResolvedValue(undefined), - readDir: vi.fn(), +const fs2Mocks = vi.hoisted(() => ({ readTextFile: vi.fn(), - writeTextFile: vi.fn().mockResolvedValue(undefined), - exists: vi.fn(), - remove: vi.fn().mockResolvedValue(undefined), + remove: vi.fn(), })); vi.mock("@hypr/plugin-path2", () => ({ commands: path2Mocks })); vi.mock("@hypr/plugin-fs-sync", () => ({ commands: fsSyncMocks })); -vi.mock("@tauri-apps/plugin-fs", () => fsMocks); +vi.mock("@hypr/plugin-fs2", () => ({ commands: fs2Mocks })); const testConfig = { tableName: "humans", diff --git a/apps/desktop/src/store/tinybase/persister/factories/markdown-dir.ts b/apps/desktop/src/store/tinybase/persister/factories/markdown-dir.ts index acadda2785..22e4036248 100644 --- a/apps/desktop/src/store/tinybase/persister/factories/markdown-dir.ts +++ b/apps/desktop/src/store/tinybase/persister/factories/markdown-dir.ts @@ -1,6 +1,6 @@ -import { readTextFile } from "@tauri-apps/plugin-fs"; import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas"; +import { commands as fs2Commands } from "@hypr/plugin-fs2"; import { commands as fsSyncCommands, type JsonValue, @@ -109,24 +109,9 @@ async function loadSingleEntity< const dataDir = await getDataDir(); const filePath = buildEntityFilePath(dataDir, dirName, entityId); - try { - const content = await readTextFile(filePath); - const parseResult = await fsSyncCommands.deserialize(content); - - if (parseResult.status === "error") { - return undefined; - } - - const entity = fromFrontmatter( - parseResult.data.frontmatter as Record, - parseResult.data.content.trim(), - ); - - return toPersistedChanges({ - [tableName]: { [entityId]: entity as Record }, - }); - } catch (error) { - if (isFileNotFoundError(error)) { + const readResult = await fs2Commands.readTextFile(filePath); + if (readResult.status === "error") { + if (isFileNotFoundError(readResult.error)) { const loaded = { [tableName]: {} } as LoadedData; const result = deletionMarker.markForEntity(loaded, entityId); @@ -136,6 +121,21 @@ async function loadSingleEntity< } return undefined; } + + const parseResult = await fsSyncCommands.deserialize(readResult.data); + + if (parseResult.status === "error") { + return undefined; + } + + const entity = fromFrontmatter( + parseResult.data.frontmatter as Record, + parseResult.data.content.trim(), + ); + + return toPersistedChanges({ + [tableName]: { [entityId]: entity as Record }, + }); } export function createMarkdownDirPersister< diff --git a/apps/desktop/src/store/tinybase/persister/factories/multi-table-dir.test.ts b/apps/desktop/src/store/tinybase/persister/factories/multi-table-dir.test.ts index f4433c8dc2..4d2ca2402b 100644 --- a/apps/desktop/src/store/tinybase/persister/factories/multi-table-dir.test.ts +++ b/apps/desktop/src/store/tinybase/persister/factories/multi-table-dir.test.ts @@ -16,18 +16,14 @@ const fsSyncMocks = vi.hoisted(() => ({ cleanupOrphan: vi.fn().mockResolvedValue({ status: "ok", data: 0 }), })); -const fsMocks = vi.hoisted(() => ({ - mkdir: vi.fn().mockResolvedValue(undefined), - readDir: vi.fn(), +const fs2Mocks = vi.hoisted(() => ({ readTextFile: vi.fn(), - writeTextFile: vi.fn().mockResolvedValue(undefined), - exists: vi.fn(), - remove: vi.fn().mockResolvedValue(undefined), + remove: vi.fn(), })); vi.mock("@hypr/plugin-path2", () => ({ commands: path2Mocks })); vi.mock("@hypr/plugin-fs-sync", () => ({ commands: fsSyncMocks })); -vi.mock("@tauri-apps/plugin-fs", () => fsMocks); +vi.mock("@hypr/plugin-fs2", () => ({ commands: fs2Mocks })); describe("createMultiTableDirPersister", () => { let store: ReturnType; diff --git a/apps/desktop/src/store/tinybase/persister/human/persister.test.ts b/apps/desktop/src/store/tinybase/persister/human/persister.test.ts index 5e0e362463..7e558d8815 100644 --- a/apps/desktop/src/store/tinybase/persister/human/persister.test.ts +++ b/apps/desktop/src/store/tinybase/persister/human/persister.test.ts @@ -19,18 +19,14 @@ const fsSyncMocks = vi.hoisted(() => ({ cleanupOrphan: vi.fn().mockResolvedValue({ status: "ok", data: 0 }), })); -const fsMocks = vi.hoisted(() => ({ - mkdir: vi.fn().mockResolvedValue(undefined), - readDir: vi.fn(), +const fs2Mocks = vi.hoisted(() => ({ readTextFile: vi.fn(), - writeTextFile: vi.fn().mockResolvedValue(undefined), - exists: vi.fn(), - remove: vi.fn().mockResolvedValue(undefined), + remove: vi.fn(), })); vi.mock("@hypr/plugin-path2", () => ({ commands: path2Mocks })); vi.mock("@hypr/plugin-fs-sync", () => ({ commands: fsSyncMocks })); -vi.mock("@tauri-apps/plugin-fs", () => fsMocks); +vi.mock("@hypr/plugin-fs2", () => ({ commands: fs2Mocks })); describe("createHumanPersister", () => { let store: ReturnType; diff --git a/apps/desktop/src/store/tinybase/persister/organization/persister.test.ts b/apps/desktop/src/store/tinybase/persister/organization/persister.test.ts index 855f16f5f5..c6a97993f9 100644 --- a/apps/desktop/src/store/tinybase/persister/organization/persister.test.ts +++ b/apps/desktop/src/store/tinybase/persister/organization/persister.test.ts @@ -15,18 +15,14 @@ const fsSyncMocks = vi.hoisted(() => ({ cleanupOrphan: vi.fn().mockResolvedValue({ status: "ok", data: 0 }), })); -const fsMocks = vi.hoisted(() => ({ - mkdir: vi.fn().mockResolvedValue(undefined), - readDir: vi.fn(), +const fs2Mocks = vi.hoisted(() => ({ readTextFile: vi.fn(), - writeTextFile: vi.fn().mockResolvedValue(undefined), - exists: vi.fn(), - remove: vi.fn().mockResolvedValue(undefined), + remove: vi.fn(), })); vi.mock("@hypr/plugin-path2", () => ({ commands: path2Mocks })); vi.mock("@hypr/plugin-fs-sync", () => ({ commands: fsSyncMocks })); -vi.mock("@tauri-apps/plugin-fs", () => fsMocks); +vi.mock("@hypr/plugin-fs2", () => ({ commands: fs2Mocks })); describe("createOrganizationPersister", () => { let store: ReturnType; diff --git a/apps/desktop/src/store/tinybase/store/importer.test.ts b/apps/desktop/src/store/tinybase/store/importer.test.ts index 247fbadc30..f82701f322 100644 --- a/apps/desktop/src/store/tinybase/store/importer.test.ts +++ b/apps/desktop/src/store/tinybase/store/importer.test.ts @@ -1,16 +1,28 @@ -import { readTextFile, remove } from "@tauri-apps/plugin-fs"; import { createMergeableStore } from "tinybase/with-schemas"; import { beforeEach, describe, expect, test, vi } from "vitest"; +import { commands as fs2Commands } from "@hypr/plugin-fs2"; +import { commands as path2Commands } from "@hypr/plugin-path2"; import { SCHEMA } from "@hypr/store"; import { importFromJson } from "./importer"; import type { Store } from "./main"; -vi.mock("@tauri-apps/plugin-fs", () => ({ - BaseDirectory: { Data: 0 }, - readTextFile: vi.fn(), - remove: vi.fn(), +vi.mock("@tauri-apps/api/path", () => ({ + sep: vi.fn(() => "/"), +})); + +vi.mock("@hypr/plugin-path2", () => ({ + commands: { + base: vi.fn().mockResolvedValue("/test/data"), + }, +})); + +vi.mock("@hypr/plugin-fs2", () => ({ + commands: { + readTextFile: vi.fn(), + remove: vi.fn(), + }, })); function createTestStore(): Store { @@ -24,14 +36,16 @@ describe("importFromJson", () => { let onPersistComplete: ReturnType; beforeEach(() => { - vi.resetAllMocks(); + vi.clearAllMocks(); + vi.mocked(path2Commands.base).mockResolvedValue("/test/data"); store = createTestStore(); onPersistComplete = vi.fn().mockResolvedValue(undefined); }); test("successfully imports data", async () => { - vi.mocked(readTextFile).mockResolvedValue( - JSON.stringify([ + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "ok", + data: JSON.stringify([ { folders: { "folder-1": { @@ -43,8 +57,11 @@ describe("importFromJson", () => { }, {}, ]), - ); - vi.mocked(remove).mockResolvedValue(undefined); + }); + vi.mocked(fs2Commands.remove).mockResolvedValue({ + status: "ok", + data: null, + }); const result = await importFromJson(store, onPersistComplete); @@ -53,15 +70,18 @@ describe("importFromJson", () => { rowsImported: 1, valuesImported: 0, }); - expect(readTextFile).toHaveBeenCalledWith("hyprnote/import.json", { - baseDir: 0, - }); + expect(fs2Commands.readTextFile).toHaveBeenCalledWith( + "/test/data/import.json", + ); expect(onPersistComplete).toHaveBeenCalledTimes(1); - expect(remove).toHaveBeenCalledWith("hyprnote/import.json", { baseDir: 0 }); + expect(fs2Commands.remove).toHaveBeenCalledWith("/test/data/import.json"); }); test("returns error for invalid JSON format - not array", async () => { - vi.mocked(readTextFile).mockResolvedValue("{}"); + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "ok", + data: "{}", + }); const result = await importFromJson(store, onPersistComplete); @@ -70,11 +90,14 @@ describe("importFromJson", () => { "expected [tables, values] array", ); expect(onPersistComplete).not.toHaveBeenCalled(); - expect(remove).not.toHaveBeenCalled(); + expect(fs2Commands.remove).not.toHaveBeenCalled(); }); test("returns error for invalid JSON format - wrong array length", async () => { - vi.mocked(readTextFile).mockResolvedValue("[1, 2, 3]"); + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "ok", + data: "[1, 2, 3]", + }); const result = await importFromJson(store, onPersistComplete); @@ -85,7 +108,10 @@ describe("importFromJson", () => { }); test("returns error when tables is not object or null", async () => { - vi.mocked(readTextFile).mockResolvedValue('["invalid", {}]'); + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "ok", + data: '["invalid", {}]', + }); const result = await importFromJson(store, onPersistComplete); @@ -96,7 +122,10 @@ describe("importFromJson", () => { }); test("returns error when values is not object or null", async () => { - vi.mocked(readTextFile).mockResolvedValue('[{}, "invalid"]'); + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "ok", + data: '[{}, "invalid"]', + }); const result = await importFromJson(store, onPersistComplete); @@ -107,8 +136,14 @@ describe("importFromJson", () => { }); test("handles null tables and values", async () => { - vi.mocked(readTextFile).mockResolvedValue("[null, null]"); - vi.mocked(remove).mockResolvedValue(undefined); + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "ok", + data: "[null, null]", + }); + vi.mocked(fs2Commands.remove).mockResolvedValue({ + status: "ok", + data: null, + }); const result = await importFromJson(store, onPersistComplete); @@ -122,10 +157,14 @@ describe("importFromJson", () => { test("merges data into existing store", async () => { store.setValues({ current_llm_provider: "existing" }); - vi.mocked(readTextFile).mockResolvedValue( - JSON.stringify([{}, { current_stt_provider: "new" }]), - ); - vi.mocked(remove).mockResolvedValue(undefined); + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "ok", + data: JSON.stringify([{}, { current_stt_provider: "new" }]), + }); + vi.mocked(fs2Commands.remove).mockResolvedValue({ + status: "ok", + data: null, + }); const result = await importFromJson(store, onPersistComplete); @@ -135,7 +174,10 @@ describe("importFromJson", () => { }); test("handles file read error", async () => { - vi.mocked(readTextFile).mockRejectedValue(new Error("File not found")); + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "error", + error: "File not found", + }); const result = await importFromJson(store, onPersistComplete); @@ -145,8 +187,14 @@ describe("importFromJson", () => { }); test("remove is called only after onPersistComplete resolves", async () => { - vi.mocked(readTextFile).mockResolvedValue("[{}, {}]"); - vi.mocked(remove).mockResolvedValue(undefined); + vi.mocked(fs2Commands.readTextFile).mockResolvedValue({ + status: "ok", + data: "[{}, {}]", + }); + vi.mocked(fs2Commands.remove).mockResolvedValue({ + status: "ok", + data: null, + }); let persistCompleted = false; const deferredPersist = vi.fn().mockImplementation(async () => { @@ -154,13 +202,14 @@ describe("importFromJson", () => { persistCompleted = true; }); - vi.mocked(remove).mockImplementation(async () => { + vi.mocked(fs2Commands.remove).mockImplementation(async () => { expect(persistCompleted).toBe(true); + return { status: "ok", data: null }; }); await importFromJson(store, deferredPersist); expect(deferredPersist).toHaveBeenCalledTimes(1); - expect(remove).toHaveBeenCalledTimes(1); + expect(fs2Commands.remove).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/desktop/src/store/tinybase/store/importer.ts b/apps/desktop/src/store/tinybase/store/importer.ts index 30fcf83d1d..cd7d185f99 100644 --- a/apps/desktop/src/store/tinybase/store/importer.ts +++ b/apps/desktop/src/store/tinybase/store/importer.ts @@ -1,13 +1,14 @@ -import { BaseDirectory, readTextFile, remove } from "@tauri-apps/plugin-fs"; +import { sep } from "@tauri-apps/api/path"; import { createMergeableStore } from "tinybase/with-schemas"; +import { commands as fs2Commands } from "@hypr/plugin-fs2"; +import { commands as path2Commands } from "@hypr/plugin-path2"; import { SCHEMA } from "@hypr/store"; import { isValidTiptapContent, md2json } from "@hypr/tiptap/shared"; import type { Store } from "./main"; -const IMPORT_PATH = "hyprnote/import.json"; -const BASE_DIR = BaseDirectory.Data; +const IMPORT_FILENAME = "import.json"; export type ImportResult = | { status: "success"; rowsImported: number; valuesImported: number } @@ -128,12 +129,26 @@ export const importFromJson = async ( onPersistComplete: () => Promise, ): Promise => { try { - const content = await readTextFile(IMPORT_PATH, { baseDir: BASE_DIR }); - const parsed = parseImportContent(content); + const base = await path2Commands.base(); + const importPath = [base, IMPORT_FILENAME].join(sep()); + + const readResult = await fs2Commands.readTextFile(importPath); + if (readResult.status === "error") { + throw new Error(readResult.error); + } + + const parsed = parseImportContent(readResult.data); const { rowsImported, valuesImported } = mergeImportData(store, parsed); await onPersistComplete(); - await remove(IMPORT_PATH, { baseDir: BASE_DIR }); + + const removeResult = await fs2Commands.remove(importPath); + if (removeResult.status === "error") { + console.warn( + "[Importer] Failed to remove import file:", + removeResult.error, + ); + } console.log( `[Importer] Successfully imported ${rowsImported} rows and ${valuesImported} values`, diff --git a/plugins/fs2/Cargo.toml b/plugins/fs2/Cargo.toml index f3e128991d..744396a838 100644 --- a/plugins/fs2/Cargo.toml +++ b/plugins/fs2/Cargo.toml @@ -2,7 +2,7 @@ name = "tauri-plugin-fs2" version = "0.1.0" authors = ["You"] -edition = "2024" +edition = "2021" exclude = ["/js", "/node_modules"] links = "tauri-plugin-fs2" description = "" @@ -15,7 +15,7 @@ specta-typescript = { workspace = true } tokio = { workspace = true, features = ["macros"] } [dependencies] -tauri-plugin-fs = { workspace = true } +tauri-plugin-path2 = { path = "../path2" } tauri = { workspace = true, features = ["test"] } tauri-specta = { workspace = true, features = ["derive", "typescript"] } diff --git a/plugins/fs2/js/bindings.gen.ts b/plugins/fs2/js/bindings.gen.ts index 987c7df2f5..404d541351 100644 --- a/plugins/fs2/js/bindings.gen.ts +++ b/plugins/fs2/js/bindings.gen.ts @@ -1,77 +1,105 @@ // @ts-nocheck + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async ping() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:fs2|ping") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async readTextFile(path: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:fs2|read_text_file", { path }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async remove(path: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:fs2|remove", { path }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + /** user-defined events **/ + + + /** user-defined constants **/ + + + /** user-defined types **/ + + + /** tauri-specta globals **/ + import { - Channel as TAURI_CHANNEL, - invoke as TAURI_INVOKE, + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; -// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. - -/** user-defined commands **/ - -export const commands = { - async ping(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("plugin:fs2|ping") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } - }, -}; - type __EventObj__ = { - listen: ( - cb: TAURI_API_EVENT.EventCallback, - ) => ReturnType>; - once: ( - cb: TAURI_API_EVENT.EventCallback, - ) => ReturnType>; - emit: null extends T - ? (payload?: T) => ReturnType - : (payload: T) => ReturnType; + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; }; export type Result = - | { status: "ok"; data: T } - | { status: "error"; error: E }; + | { status: "ok"; data: T } + | { status: "error"; error: E }; function __makeEvents__>( - mappings: Record, + mappings: Record, ) { - return new Proxy( - {} as unknown as { - [K in keyof T]: __EventObj__ & { - (handle: __WebviewWindow__): __EventObj__; - }; - }, - { - get: (_, event) => { - const name = mappings[event as keyof T]; - - return new Proxy((() => {}) as any, { - apply: (_, __, [window]: [__WebviewWindow__]) => ({ - listen: (arg: any) => window.listen(name, arg), - once: (arg: any) => window.once(name, arg), - emit: (arg: any) => window.emit(name, arg), - }), - get: (_, command: keyof __EventObj__) => { - switch (command) { - case "listen": - return (arg: any) => TAURI_API_EVENT.listen(name, arg); - case "once": - return (arg: any) => TAURI_API_EVENT.once(name, arg); - case "emit": - return (arg: any) => TAURI_API_EVENT.emit(name, arg); - } - }, - }); - }, - }, - ); + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); } diff --git a/plugins/fs2/src/commands.rs b/plugins/fs2/src/commands.rs index d36009dbbb..160ba0e0a8 100644 --- a/plugins/fs2/src/commands.rs +++ b/plugins/fs2/src/commands.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use crate::Fs2PluginExt; #[tauri::command] @@ -5,3 +7,23 @@ use crate::Fs2PluginExt; pub(crate) async fn ping(app: tauri::AppHandle) -> Result { app.fs2().ping().map_err(|e| e.to_string()) } + +#[tauri::command] +#[specta::specta] +pub(crate) async fn read_text_file( + app: tauri::AppHandle, + path: String, +) -> Result { + let path = PathBuf::from(path); + app.fs2().read_text_file(&path).map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn remove( + app: tauri::AppHandle, + path: String, +) -> Result<(), String> { + let path = PathBuf::from(path); + app.fs2().remove(&path).map_err(|e| e.to_string()) +} diff --git a/plugins/fs2/src/error.rs b/plugins/fs2/src/error.rs index 53206e2ee5..546936d975 100644 --- a/plugins/fs2/src/error.rs +++ b/plugins/fs2/src/error.rs @@ -6,6 +6,10 @@ pub type Result = std::result::Result; pub enum Error { #[error(transparent)] Io(#[from] std::io::Error), + #[error("path forbidden: {0}")] + PathForbidden(std::path::PathBuf), + #[error("path error: {0}")] + Path(String), } impl Serialize for Error { diff --git a/plugins/fs2/src/ext.rs b/plugins/fs2/src/ext.rs index 3694a8bd68..ac8c24a6dc 100644 --- a/plugins/fs2/src/ext.rs +++ b/plugins/fs2/src/ext.rs @@ -1,5 +1,8 @@ +use std::path::{Path, PathBuf}; + +use tauri_plugin_path2::Path2PluginExt; + pub struct Fs2<'a, R: tauri::Runtime, M: tauri::Manager> { - #[allow(dead_code)] manager: &'a M, _runtime: std::marker::PhantomData R>, } @@ -8,6 +11,73 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager> Fs2<'a, R, M> { pub fn ping(&self) -> Result { Ok("pong".to_string()) } + + fn base(&self) -> Result { + self.manager + .path2() + .base() + .map_err(|e| crate::Error::Path(e.to_string())) + } + + fn validate_path(&self, path: &Path) -> Result { + let base = self.base()?; + + let resolved = if path.is_absolute() { + path.to_path_buf() + } else { + base.join(path) + }; + + let canonical_base = base.canonicalize().unwrap_or_else(|_| base.clone()); + + let canonical_path = + if resolved.exists() { + resolved.canonicalize()? + } else { + let parent = resolved.parent().ok_or_else(|| { + crate::Error::Path("invalid path: no parent directory".to_string()) + })?; + + if parent.exists() { + let canonical_parent = parent.canonicalize()?; + canonical_parent.join(resolved.file_name().ok_or_else(|| { + crate::Error::Path("invalid path: no file name".to_string()) + })?) + } else { + resolved.clone() + } + }; + + if canonical_path.starts_with(&canonical_base) { + Ok(resolved) + } else { + Err(crate::Error::PathForbidden(resolved)) + } + } + + pub fn read_text_file(&self, path: &Path) -> Result { + let validated_path = self.validate_path(path)?; + let content = std::fs::read_to_string(&validated_path)?; + Ok(content) + } + + pub fn remove(&self, path: &Path) -> Result<(), crate::Error> { + let validated_path = self.validate_path(path)?; + + if !validated_path.exists() { + return Ok(()); + } + + let metadata = std::fs::symlink_metadata(&validated_path)?; + + if metadata.is_dir() { + std::fs::remove_dir_all(&validated_path)?; + } else { + std::fs::remove_file(&validated_path)?; + } + + Ok(()) + } } pub trait Fs2PluginExt { diff --git a/plugins/fs2/src/lib.rs b/plugins/fs2/src/lib.rs index 4ddb0f08a4..72d1b074b4 100644 --- a/plugins/fs2/src/lib.rs +++ b/plugins/fs2/src/lib.rs @@ -12,6 +12,8 @@ fn make_specta_builder() -> tauri_specta::Builder { .plugin_name(PLUGIN_NAME) .commands(tauri_specta::collect_commands![ commands::ping::, + commands::read_text_file::, + commands::remove::, ]) .error_handling(tauri_specta::ErrorHandlingMode::Result) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bbf662262..7042302e09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -419,9 +419,6 @@ importers: '@tauri-apps/plugin-dialog': specifier: ^2.4.2 version: 2.4.2 - '@tauri-apps/plugin-fs': - specifier: ^2.4.4 - version: 2.4.4 '@tauri-apps/plugin-http': specifier: ^2.5.4 version: 2.5.4 @@ -7504,9 +7501,6 @@ packages: '@tauri-apps/plugin-dialog@2.4.2': resolution: {integrity: sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==} - '@tauri-apps/plugin-fs@2.4.4': - resolution: {integrity: sha512-MTorXxIRmOnOPT1jZ3w96vjSuScER38ryXY88vl5F0uiKdnvTKKTtaEjTEo8uPbl4e3gnUtfsDVwC7h77GQLvQ==} - '@tauri-apps/plugin-http@2.5.4': resolution: {integrity: sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg==} @@ -23299,10 +23293,6 @@ snapshots: dependencies: '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-fs@2.4.4': - dependencies: - '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-http@2.5.4': dependencies: '@tauri-apps/api': 2.9.1 From a9627bf656a31c6236e3fecbd07442ac4fa630e2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:53:03 +0000 Subject: [PATCH 4/7] refactor: remove ping command from fs2 plugin, update build.rs and permissions - Remove ping command from commands.rs, ext.rs, and lib.rs - Update build.rs to list read_text_file and remove commands - Update default.toml to list allow-read-text-file and allow-remove permissions - Remove ping.toml and add read_text_file.toml and remove.toml permission files - Regenerate TypeScript bindings Co-Authored-By: yujonglee --- plugins/fs2/build.rs | 2 +- plugins/fs2/js/bindings.gen.ts | 8 --- .../autogenerated/commands/ping.toml | 13 ----- .../commands/read_text_file.toml | 13 +++++ .../autogenerated/commands/remove.toml | 13 +++++ .../permissions/autogenerated/reference.md | 57 ++++++++++++++++++- plugins/fs2/permissions/default.toml | 4 +- plugins/fs2/permissions/schemas/schema.json | 28 ++++++++- plugins/fs2/src/commands.rs | 6 -- plugins/fs2/src/ext.rs | 4 -- plugins/fs2/src/lib.rs | 16 ------ 11 files changed, 110 insertions(+), 54 deletions(-) delete mode 100644 plugins/fs2/permissions/autogenerated/commands/ping.toml create mode 100644 plugins/fs2/permissions/autogenerated/commands/read_text_file.toml create mode 100644 plugins/fs2/permissions/autogenerated/commands/remove.toml diff --git a/plugins/fs2/build.rs b/plugins/fs2/build.rs index 029861396b..c939b90725 100644 --- a/plugins/fs2/build.rs +++ b/plugins/fs2/build.rs @@ -1,4 +1,4 @@ -const COMMANDS: &[&str] = &["ping"]; +const COMMANDS: &[&str] = &["read_text_file", "remove"]; fn main() { tauri_plugin::Builder::new(COMMANDS).build(); diff --git a/plugins/fs2/js/bindings.gen.ts b/plugins/fs2/js/bindings.gen.ts index 404d541351..84c4ef50cc 100644 --- a/plugins/fs2/js/bindings.gen.ts +++ b/plugins/fs2/js/bindings.gen.ts @@ -6,14 +6,6 @@ export const commands = { -async ping() : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("plugin:fs2|ping") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async readTextFile(path: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("plugin:fs2|read_text_file", { path }) }; diff --git a/plugins/fs2/permissions/autogenerated/commands/ping.toml b/plugins/fs2/permissions/autogenerated/commands/ping.toml deleted file mode 100644 index 1d1358807e..0000000000 --- a/plugins/fs2/permissions/autogenerated/commands/ping.toml +++ /dev/null @@ -1,13 +0,0 @@ -# Automatically generated - DO NOT EDIT! - -"$schema" = "../../schemas/schema.json" - -[[permission]] -identifier = "allow-ping" -description = "Enables the ping command without any pre-configured scope." -commands.allow = ["ping"] - -[[permission]] -identifier = "deny-ping" -description = "Denies the ping command without any pre-configured scope." -commands.deny = ["ping"] diff --git a/plugins/fs2/permissions/autogenerated/commands/read_text_file.toml b/plugins/fs2/permissions/autogenerated/commands/read_text_file.toml new file mode 100644 index 0000000000..7a25115db9 --- /dev/null +++ b/plugins/fs2/permissions/autogenerated/commands/read_text_file.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-read-text-file" +description = "Enables the read_text_file command without any pre-configured scope." +commands.allow = ["read_text_file"] + +[[permission]] +identifier = "deny-read-text-file" +description = "Denies the read_text_file command without any pre-configured scope." +commands.deny = ["read_text_file"] diff --git a/plugins/fs2/permissions/autogenerated/commands/remove.toml b/plugins/fs2/permissions/autogenerated/commands/remove.toml new file mode 100644 index 0000000000..9c9791ebcb --- /dev/null +++ b/plugins/fs2/permissions/autogenerated/commands/remove.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-remove" +description = "Enables the remove command without any pre-configured scope." +commands.allow = ["remove"] + +[[permission]] +identifier = "deny-remove" +description = "Denies the remove command without any pre-configured scope." +commands.deny = ["remove"] diff --git a/plugins/fs2/permissions/autogenerated/reference.md b/plugins/fs2/permissions/autogenerated/reference.md index 69e2153b30..a780651e3a 100644 --- a/plugins/fs2/permissions/autogenerated/reference.md +++ b/plugins/fs2/permissions/autogenerated/reference.md @@ -1,10 +1,11 @@ ## Default Permission -Default permissions for the plugin +Default permissions for the fs2 plugin #### This default permission set includes the following: -- `allow-ping` +- `allow-read-text-file` +- `allow-remove` ## Permission Table @@ -38,6 +39,58 @@ Enables the ping command without any pre-configured scope. Denies the ping command without any pre-configured scope. + + + + + + +`fs2:allow-read-text-file` + + + + +Enables the read_text_file command without any pre-configured scope. + + + + + + + +`fs2:deny-read-text-file` + + + + +Denies the read_text_file command without any pre-configured scope. + + + + + + + +`fs2:allow-remove` + + + + +Enables the remove command without any pre-configured scope. + + + + + + + +`fs2:deny-remove` + + + + +Denies the remove command without any pre-configured scope. + diff --git a/plugins/fs2/permissions/default.toml b/plugins/fs2/permissions/default.toml index cc5a76f22e..7acacae56f 100644 --- a/plugins/fs2/permissions/default.toml +++ b/plugins/fs2/permissions/default.toml @@ -1,3 +1,3 @@ [default] -description = "Default permissions for the plugin" -permissions = ["allow-ping"] +description = "Default permissions for the fs2 plugin" +permissions = ["allow-read-text-file", "allow-remove"] diff --git a/plugins/fs2/permissions/schemas/schema.json b/plugins/fs2/permissions/schemas/schema.json index ac68e129e2..23e8223a53 100644 --- a/plugins/fs2/permissions/schemas/schema.json +++ b/plugins/fs2/permissions/schemas/schema.json @@ -307,10 +307,34 @@ "markdownDescription": "Denies the ping command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`", + "description": "Enables the read_text_file command without any pre-configured scope.", + "type": "string", + "const": "allow-read-text-file", + "markdownDescription": "Enables the read_text_file command without any pre-configured scope." + }, + { + "description": "Denies the read_text_file command without any pre-configured scope.", + "type": "string", + "const": "deny-read-text-file", + "markdownDescription": "Denies the read_text_file command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Default permissions for the fs2 plugin\n#### This default permission set includes:\n\n- `allow-read-text-file`\n- `allow-remove`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`" + "markdownDescription": "Default permissions for the fs2 plugin\n#### This default permission set includes:\n\n- `allow-read-text-file`\n- `allow-remove`" } ] } diff --git a/plugins/fs2/src/commands.rs b/plugins/fs2/src/commands.rs index 160ba0e0a8..884acb258f 100644 --- a/plugins/fs2/src/commands.rs +++ b/plugins/fs2/src/commands.rs @@ -2,12 +2,6 @@ use std::path::PathBuf; use crate::Fs2PluginExt; -#[tauri::command] -#[specta::specta] -pub(crate) async fn ping(app: tauri::AppHandle) -> Result { - app.fs2().ping().map_err(|e| e.to_string()) -} - #[tauri::command] #[specta::specta] pub(crate) async fn read_text_file( diff --git a/plugins/fs2/src/ext.rs b/plugins/fs2/src/ext.rs index ac8c24a6dc..dba94dac51 100644 --- a/plugins/fs2/src/ext.rs +++ b/plugins/fs2/src/ext.rs @@ -8,10 +8,6 @@ pub struct Fs2<'a, R: tauri::Runtime, M: tauri::Manager> { } impl<'a, R: tauri::Runtime, M: tauri::Manager> Fs2<'a, R, M> { - pub fn ping(&self) -> Result { - Ok("pong".to_string()) - } - fn base(&self) -> Result { self.manager .path2() diff --git a/plugins/fs2/src/lib.rs b/plugins/fs2/src/lib.rs index 72d1b074b4..b2aba28480 100644 --- a/plugins/fs2/src/lib.rs +++ b/plugins/fs2/src/lib.rs @@ -11,7 +11,6 @@ fn make_specta_builder() -> tauri_specta::Builder { tauri_specta::Builder::::new() .plugin_name(PLUGIN_NAME) .commands(tauri_specta::collect_commands![ - commands::ping::, commands::read_text_file::, commands::remove::, ]) @@ -47,19 +46,4 @@ mod test { std::fs::write(OUTPUT_FILE, format!("// @ts-nocheck\n{content}")).unwrap(); } - fn create_app(builder: tauri::Builder) -> tauri::App { - let mut ctx = tauri::test::mock_context(tauri::test::noop_assets()); - ctx.config_mut().identifier = "com.hyprnote.dev".to_string(); - ctx.config_mut().version = Some("0.0.1".to_string()); - - builder.plugin(init()).build(ctx).unwrap() - } - - #[tokio::test] - async fn test_ping() { - let app = create_app(tauri::test::mock_builder()); - let result = app.fs2().ping(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "pong"); - } } From bd9e6075ec648c3ed38a3a661cc3dd68cfd14bd0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:00:31 +0000 Subject: [PATCH 5/7] fix: remove extra blank line in lib.rs for dprint formatting Co-Authored-By: yujonglee --- plugins/fs2/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/fs2/src/lib.rs b/plugins/fs2/src/lib.rs index 4ddb0f08a4..dd6af1a810 100644 --- a/plugins/fs2/src/lib.rs +++ b/plugins/fs2/src/lib.rs @@ -44,7 +44,6 @@ mod test { let content = std::fs::read_to_string(OUTPUT_FILE).unwrap(); std::fs::write(OUTPUT_FILE, format!("// @ts-nocheck\n{content}")).unwrap(); } - fn create_app(builder: tauri::Builder) -> tauri::App { let mut ctx = tauri::test::mock_context(tauri::test::noop_assets()); ctx.config_mut().identifier = "com.hyprnote.dev".to_string(); From 7801ca105d86679bdce6238abaca5bb89250d3d5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:15:28 +0000 Subject: [PATCH 6/7] refactor: remove tauri-plugin-fs dependency and simplify capabilities - Remove tauri-plugin-fs from Cargo.toml and lib.rs - Remove all fs: permissions from capabilities/default.json - fs2 plugin now handles path validation in Rust, no need for permission globs Co-Authored-By: yujonglee --- apps/desktop/src-tauri/Cargo.toml | 1 - .../src-tauri/capabilities/default.json | 188 ++---------------- apps/desktop/src-tauri/src/lib.rs | 1 - 3 files changed, 13 insertions(+), 177 deletions(-) diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 3cd784d998..455f5c0bad 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -36,7 +36,6 @@ tauri-plugin-deeplink2 = { workspace = true } tauri-plugin-detect = { workspace = true } tauri-plugin-dialog = { workspace = true } tauri-plugin-extensions = { workspace = true } -tauri-plugin-fs = { workspace = true } tauri-plugin-fs-sync = { workspace = true } tauri-plugin-fs2 = { workspace = true } tauri-plugin-hooks = { workspace = true } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 899ee230a4..dc2f231903 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -35,183 +35,21 @@ } ] }, - { - "identifier": "opener:allow-open-path", - "allow": [ - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA/**/*" - }, - { - "path": "$DOWNLOAD/**/*" - } - ] - }, - { - "identifier": "fs:allow-mkdir", - "allow": [ - { - "path": "$DATA/hyprnote" - }, - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" - }, - { - "path": "$APPDATA/**/*" - } - ] - }, - { - "identifier": "fs:allow-read-dir", - "allow": [ - { - "path": "$DATA/hyprnote" - }, - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" - }, - { - "path": "$APPDATA/**/*" - } - ] - }, - { - "identifier": "fs:allow-exists", - "allow": [ - { - "path": "/Applications/*" - }, - { - "path": "$DATA/hyprnote" - }, - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" - }, - { - "path": "$APPDATA/**/*" - } - ] - }, - { - "identifier": "fs:allow-read-file", - "allow": [ - { - "path": "$DATA/hyprnote" - }, - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" - }, - { - "path": "$APPDATA/**/*" - } - ] - }, - { - "identifier": "fs:allow-read-text-file", - "allow": [ - { - "path": "$DATA/hyprnote" - }, { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" - }, - { - "path": "$APPDATA/**/*" - } - ] - }, - { - "identifier": "fs:allow-write-text-file", - "allow": [ - { - "path": "$DATA/hyprnote" - }, - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" - }, - { - "path": "$APPDATA/**/*" - }, - { - "path": "$DOWNLOAD/**/*" - } - ] - }, - { - "identifier": "fs:allow-write-file", - "allow": [ - { - "path": "$DATA/hyprnote" - }, - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" - }, - { - "path": "$APPDATA/**/*" - }, - { - "path": "$DOWNLOAD/**/*" - } - ] - }, - { - "identifier": "fs:allow-remove", - "allow": [ - { - "path": "$DATA/hyprnote" - }, - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" - }, - { - "path": "$APPDATA/**/*" - } - ] - }, - { - "identifier": "fs:allow-rename", - "allow": [ - { - "path": "$DATA/hyprnote" - }, - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA" + "identifier": "opener:allow-open-path", + "allow": [ + { + "path": "$DATA/hyprnote/**/*" + }, + { + "path": "$APPDATA/**/*" + }, + { + "path": "$DOWNLOAD/**/*" + } + ] }, - { - "path": "$APPDATA/**/*" - } - ] - }, - "apple-calendar:default", + "apple-calendar:default", "apple-contact:default", "audio-priority:default", "auth:default", diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 5c7fac732a..663ba81c1d 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -106,7 +106,6 @@ pub async fn main() { .plugin(tauri_plugin_fs_sync::init()) .plugin(tauri_plugin_fs2::init()) .plugin(tauri_plugin_os::init()) - .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_path2::init()) .plugin(tauri_plugin_pdf::init()) .plugin(tauri_plugin_process::init()) From d35efd62a7989591bb39747919f909ea7a8188f0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:22:21 +0000 Subject: [PATCH 7/7] fix: format default.json with dprint Co-Authored-By: yujonglee --- .../src-tauri/capabilities/default.json | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index dc2f231903..6b98241617 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -35,21 +35,21 @@ } ] }, + { + "identifier": "opener:allow-open-path", + "allow": [ { - "identifier": "opener:allow-open-path", - "allow": [ - { - "path": "$DATA/hyprnote/**/*" - }, - { - "path": "$APPDATA/**/*" - }, - { - "path": "$DOWNLOAD/**/*" - } - ] + "path": "$DATA/hyprnote/**/*" }, - "apple-calendar:default", + { + "path": "$APPDATA/**/*" + }, + { + "path": "$DOWNLOAD/**/*" + } + ] + }, + "apple-calendar:default", "apple-contact:default", "audio-priority:default", "auth:default",