Merge branch 'tauri-apps:v2' into v2

pull/1383/head
nvzeppelin 11 months ago committed by GitHub
commit d768bddc5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -68,6 +68,7 @@
"os", "os",
"process", "process",
"shell", "shell",
"store",
"updater" "updater"
] ]
}, },
@ -90,6 +91,7 @@
"os-js", "os-js",
"process-js", "process-js",
"shell-js", "shell-js",
"store-js",
"updater-js" "updater-js"
], ],
"postversion": "pnpm install --no-frozen-lockfile" "postversion": "pnpm install --no-frozen-lockfile"

@ -0,0 +1,5 @@
---
"dialog": "patch"
---
Mark `FileResponse` as `non_exhaustive`.

@ -0,0 +1,7 @@
---
"dialog": patch
"dialog-js": patch
---
The `open` function now returns a string representing either the file path or URI instead of an object.
To read the file data, use the `fs` APIs.

@ -0,0 +1,5 @@
---
"fs": patch:enhance
---
The `scope-*-recursive` permissions now also allow reading the contents of the directory.

@ -0,0 +1,5 @@
---
"global-shortcut": "patch"
---
Updated `global-hotkey` crate dependency to `0.6`

@ -11,6 +11,7 @@
".changes/remove-target-sdk.md", ".changes/remove-target-sdk.md",
".changes/shell-open-regex-match-string.md", ".changes/shell-open-regex-match-string.md",
".changes/shell-regex-match-string.md", ".changes/shell-regex-match-string.md",
".changes/swift-build-older-versions.md",
".changes/update-fs-api-docs.md", ".changes/update-fs-api-docs.md",
".changes/update-tauri-rc-3.md" ".changes/update-tauri-rc-3.md"
] ]

@ -0,0 +1,5 @@
---
"fs": patch:feat
---
Resolve `content://` path URIs on Android.

@ -0,0 +1,5 @@
---
"single-instance": "patch"
---
Updated `windows-sys` crate to `0.59`

@ -0,0 +1,5 @@
---
"store": patch:breaking
---
Implement mobile support in Rust directly. This changes the store directories, invalidating all previously generated stores.

@ -0,0 +1,15 @@
---
"barcode-scanner": patch
"biometric": patch
"clipboard-manager": patch
"dialog": patch
"geolocation": patch
"haptics": patch
"log-plugin": patch
"nfc": patch
"notification": patch
"shell": patch
"store": patch
---
Explicitly set a minimum macOS version for the Swift package.

178
Cargo.lock generated

@ -206,7 +206,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "api" name = "api"
version = "2.0.0-rc.0" version = "2.0.0-rc.1"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -227,6 +227,7 @@ dependencies = [
"tauri-plugin-os", "tauri-plugin-os",
"tauri-plugin-process", "tauri-plugin-process",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-store",
"tauri-plugin-updater", "tauri-plugin-updater",
"tiny_http", "tiny_http",
"window-shadows", "window-shadows",
@ -2565,19 +2566,19 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "global-hotkey" name = "global-hotkey"
version = "0.5.5" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b436093d1598b05e3b7fddc097b2bad32763f53a1beb25ab6f9718c6a60acd09" checksum = "298a7667d6011efe6b35673c6b29001b88677ae1b3d6b2feccfbff4b44892866"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cocoa 0.25.0", "cocoa 0.26.0",
"crossbeam-channel", "crossbeam-channel",
"keyboard-types", "keyboard-types",
"objc", "objc",
"once_cell", "once_cell",
"serde", "serde",
"thiserror", "thiserror",
"windows-sys 0.52.0", "windows-sys 0.59.0",
"x11-dl", "x11-dl",
] ]
@ -2865,13 +2866,13 @@ dependencies = [
"http", "http",
"hyper", "hyper",
"hyper-util", "hyper-util",
"rustls 0.23.12", "rustls",
"rustls-native-certs", "rustls-native-certs",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots 0.26.3", "webpki-roots",
] ]
[[package]] [[package]]
@ -3440,9 +3441,9 @@ dependencies = [
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.28.0" version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",
@ -4713,7 +4714,7 @@ dependencies = [
"quinn-proto", "quinn-proto",
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls 0.23.12", "rustls",
"socket2", "socket2",
"thiserror", "thiserror",
"tokio", "tokio",
@ -4730,7 +4731,7 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"ring", "ring",
"rustc-hash", "rustc-hash",
"rustls 0.23.12", "rustls",
"slab", "slab",
"thiserror", "thiserror",
"tinyvec", "tinyvec",
@ -5004,9 +5005,9 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
"rustls 0.23.12", "rustls",
"rustls-native-certs", "rustls-native-certs",
"rustls-pemfile 2.1.3", "rustls-pemfile",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
@ -5024,7 +5025,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"webpki-roots 0.26.3", "webpki-roots",
"winreg 0.52.0", "winreg 0.52.0",
] ]
@ -5222,17 +5223,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"ring",
"rustls-webpki 0.101.7",
"sct",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.12" version = "0.23.12"
@ -5242,7 +5232,7 @@ dependencies = [
"once_cell", "once_cell",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki 0.102.6", "rustls-webpki",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@ -5254,21 +5244,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pemfile 2.1.3", "rustls-pemfile",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework", "security-framework",
] ]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "2.1.3" version = "2.1.3"
@ -5285,16 +5266,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.102.6" version = "0.102.6"
@ -5406,16 +5377,6 @@ dependencies = [
"sha2", "sha2",
] ]
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "seahash" name = "seahash"
version = "4.1.0" version = "4.1.0"
@ -5863,9 +5824,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx" name = "sqlx"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c" checksum = "fcfa89bea9500db4a0d038513d7a060566bfc51d46d1c014847049a45cce85e8"
dependencies = [ dependencies = [
"sqlx-core", "sqlx-core",
"sqlx-macros", "sqlx-macros",
@ -5876,9 +5837,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-core" name = "sqlx-core"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b" checksum = "d06e2f2bd861719b1f3f0c7dbe1d80c30bf59e76cf019f07d9014ed7eefb8e08"
dependencies = [ dependencies = [
"atoi", "atoi",
"byteorder", "byteorder",
@ -5901,8 +5862,8 @@ dependencies = [
"once_cell", "once_cell",
"paste", "paste",
"percent-encoding", "percent-encoding",
"rustls 0.21.12", "rustls",
"rustls-pemfile 1.0.4", "rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -5914,14 +5875,14 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tracing", "tracing",
"url", "url",
"webpki-roots 0.25.4", "webpki-roots",
] ]
[[package]] [[package]]
name = "sqlx-macros" name = "sqlx-macros"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88" checksum = "2f998a9defdbd48ed005a89362bd40dd2117502f15294f61c8d47034107dbbdc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5932,9 +5893,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-macros-core" name = "sqlx-macros-core"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" checksum = "3d100558134176a2629d46cec0c8891ba0be8910f7896abfdb75ef4ab6f4e7ce"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"either", "either",
@ -5958,9 +5919,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-mysql" name = "sqlx-mysql"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" checksum = "936cac0ab331b14cb3921c62156d913e4c15b74fb6ec0f3146bd4ef6e4fb3c12"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64 0.22.1", "base64 0.22.1",
@ -6001,9 +5962,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-postgres" name = "sqlx-postgres"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" checksum = "9734dbce698c67ecf67c442f768a5e90a49b2a4d61a9f1d59f73874bd4cf0710"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64 0.22.1", "base64 0.22.1",
@ -6040,9 +6001,9 @@ dependencies = [
[[package]] [[package]]
name = "sqlx-sqlite" name = "sqlx-sqlite"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee" checksum = "a75b419c3c1b1697833dd927bdc4c6545a620bc1bbafabd44e1efbe9afcd337e"
dependencies = [ dependencies = [
"atoi", "atoi",
"flume", "flume",
@ -6359,9 +6320,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.0.0-rc.3" version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79776954e2cd6b6c3b56e2cd99905a3a166017495a39ac8eb4c85dd8ea8704b4" checksum = "7d386b956b09cf88301912453829269f3914b3c813020d429ed8110c75e9dded"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -6412,9 +6373,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.0.0-rc.3" version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc103bde77870e08d5fc8765615b9615997827550b626fbc4ebbd7a1fbfe2a2" checksum = "1e79aafbbfc8262d7937675cb44c397e975ab8e0cd722db1c37de694fd443570"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@ -6436,9 +6397,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.0.0-rc.3" version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea061e6be9b37ab455eadc189f45617deafc85c94f78f9cd584862a6deaa83d1" checksum = "5ce4e521130c5d7b377ddfdc43310ece626b67ec07ae74174407ad7e6cd17d20"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli", "brotli",
@ -6463,9 +6424,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.0.0-rc.3" version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e20d6f6f96f55a43339c465b3c8205d71940372d54d7c665c5329e8e4ba35d0" checksum = "a5995206394cd30411fc5c8ae195e498357f63e11ed960ea32b53512dcb2a5a5"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -6477,9 +6438,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin" name = "tauri-plugin"
version = "2.0.0-rc.3" version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec01af01098a286d3e430c1fa947bfd77bc8011ecb209438af4444b02d82b29e" checksum = "0ebbdbf4e6d7328e0c0f2427b4f56d792ee1ae84ab4fb0286b81a2e408836046"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@ -6530,7 +6491,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-barcode-scanner" name = "tauri-plugin-barcode-scanner"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -6542,7 +6503,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-biometric" name = "tauri-plugin-biometric"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -6568,7 +6529,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-clipboard-manager" name = "tauri-plugin-clipboard-manager"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"arboard", "arboard",
"image 0.24.9", "image 0.24.9",
@ -6600,7 +6561,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"dunce", "dunce",
"log", "log",
@ -6612,6 +6573,7 @@ dependencies = [
"tauri-plugin", "tauri-plugin",
"tauri-plugin-fs", "tauri-plugin-fs",
"thiserror", "thiserror",
"url",
] ]
[[package]] [[package]]
@ -6635,7 +6597,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-geolocation" name = "tauri-plugin-geolocation"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -6661,7 +6623,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-haptics" name = "tauri-plugin-haptics"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -6707,7 +6669,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-log" name = "tauri-plugin-log"
version = "2.0.0-rc.0" version = "2.0.0-rc.1"
dependencies = [ dependencies = [
"android_logger", "android_logger",
"byte-unit", "byte-unit",
@ -6727,7 +6689,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-nfc" name = "tauri-plugin-nfc"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -6740,7 +6702,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-notification" name = "tauri-plugin-notification"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"color-backtrace", "color-backtrace",
"ctor", "ctor",
@ -6813,7 +6775,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
@ -6840,7 +6802,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"thiserror", "thiserror",
"windows-sys 0.52.0", "windows-sys 0.59.0",
"zbus", "zbus",
] ]
@ -6863,7 +6825,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-store" name = "tauri-plugin-store"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
dependencies = [ dependencies = [
"dunce", "dunce",
"log", "log",
@ -6971,9 +6933,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.0.0-rc.3" version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4e736d3293f8347e5d2c5b250fe0e5b873499f5483578b139445dbbf802e2e5" checksum = "6e49398fb1d7736e41099aa7efaf45d599e480a36b3e7f88977b547b662d7253"
dependencies = [ dependencies = [
"dpi", "dpi",
"gtk", "gtk",
@ -6990,9 +6952,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.0.0-rc.3" version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fead81c1bd0205d5f02580e64f522704618274e784c2d1c127e4ba19acd0b79" checksum = "8b2ce1dca90243bd4a77a1020847688590e1ded2f6d190d5a96877b0039f0500"
dependencies = [ dependencies = [
"cocoa 0.26.0", "cocoa 0.26.0",
"gtk", "gtk",
@ -7014,9 +6976,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.0.0-rc.3" version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "285af18e09665ea15fdda04cb28fb579a4d71b4e1640628489fecca98838ca9a" checksum = "2d702b62eed4cf89034926cb1834e2d13a7d745ea08a457fd336f94cde48f2fb"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"brotli", "brotli",
@ -7265,7 +7227,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [ dependencies = [
"rustls 0.23.12", "rustls",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
] ]
@ -7302,14 +7264,14 @@ dependencies = [
"futures-util", "futures-util",
"log", "log",
"native-tls", "native-tls",
"rustls 0.23.12", "rustls",
"rustls-native-certs", "rustls-native-certs",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tungstenite", "tungstenite",
"webpki-roots 0.26.3", "webpki-roots",
] ]
[[package]] [[package]]
@ -7520,7 +7482,7 @@ dependencies = [
"log", "log",
"native-tls", "native-tls",
"rand 0.8.5", "rand 0.8.5",
"rustls 0.23.12", "rustls",
"rustls-pki-types", "rustls-pki-types",
"sha1", "sha1",
"thiserror", "thiserror",
@ -7962,12 +7924,6 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "webpki-roots"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.26.3" version = "0.26.3"

@ -11,10 +11,10 @@ resolver = "2"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
log = "0.4" log = "0.4"
tauri = { version = "2.0.0-rc.3", default-features = false } tauri = { version = "2.0.0-rc.5", default-features = false }
tauri-build = "2.0.0-rc.3" tauri-build = "2.0.0-rc.5"
tauri-plugin = "2.0.0-rc.3" tauri-plugin = "2.0.0-rc.5"
tauri-utils = "2.0.0-rc.3" tauri-utils = "2.0.0-rc.5"
serde_json = "1" serde_json = "1"
thiserror = "1" thiserror = "1"
url = "2" url = "2"

@ -23,6 +23,7 @@
"@tauri-apps/plugin-os": "2.0.0-rc.0", "@tauri-apps/plugin-os": "2.0.0-rc.0",
"@tauri-apps/plugin-process": "2.0.0-rc.0", "@tauri-apps/plugin-process": "2.0.0-rc.0",
"@tauri-apps/plugin-shell": "2.0.0-rc.0", "@tauri-apps/plugin-shell": "2.0.0-rc.0",
"@tauri-apps/plugin-store": "2.0.0-rc.0",
"@tauri-apps/plugin-updater": "2.0.0-rc.0", "@tauri-apps/plugin-updater": "2.0.0-rc.0",
"@zerodevx/svelte-json-view": "1.0.9" "@zerodevx/svelte-json-view": "1.0.9"
}, },
@ -30,7 +31,7 @@
"@iconify-json/codicon": "^1.1.37", "@iconify-json/codicon": "^1.1.37",
"@iconify-json/ph": "^1.1.8", "@iconify-json/ph": "^1.1.8",
"@sveltejs/vite-plugin-svelte": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tauri-apps/cli": "2.0.0-rc.4", "@tauri-apps/cli": "2.0.0-rc.6",
"@unocss/extractor-svelte": "^0.62.0", "@unocss/extractor-svelte": "^0.62.0",
"svelte": "^4.2.8", "svelte": "^4.2.8",
"unocss": "^0.62.0", "unocss": "^0.62.0",

@ -1,5 +1,18 @@
# Changelog # Changelog
## \[2.0.0-rc.1]
### Dependencies
- Upgraded to `barcode-scanner@2.0.0-rc.2`
- Upgraded to `biometric@2.0.0-rc.2`
- Upgraded to `clipboard-manager@2.0.0-rc.2`
- Upgraded to `dialog@2.0.0-rc.2`
- Upgraded to `log-plugin@2.0.0-rc.1`
- Upgraded to `nfc@2.0.0-rc.2`
- Upgraded to `notification@2.0.0-rc.2`
- Upgraded to `shell@2.0.0-rc.2`
## \[2.0.0-rc.0] ## \[2.0.0-rc.0]
### Dependencies ### Dependencies

@ -1,7 +1,7 @@
[package] [package]
name = "api" name = "api"
publish = false publish = false
version = "2.0.0-rc.0" version = "2.0.0-rc.1"
description = "An example Tauri Application showcasing the api" description = "An example Tauri Application showcasing the api"
edition = "2021" edition = "2021"
rust-version = { workspace = true } rust-version = { workspace = true }
@ -19,21 +19,22 @@ serde_json = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tiny_http = "0.12" tiny_http = "0.12"
log = { workspace = true } log = { workspace = true }
tauri-plugin-log = { path = "../../../plugins/log", version = "2.0.0-rc.0" } tauri-plugin-log = { path = "../../../plugins/log", version = "2.0.0-rc.1" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.0.0-rc.0", features = [ tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.0.0-rc.0", features = [
"watch", "watch",
] } ] }
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.0.0-rc.1" } tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.0.0-rc.2" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.0.0-rc.1" } tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.0.0-rc.2" }
tauri-plugin-http = { path = "../../../plugins/http", features = [ tauri-plugin-http = { path = "../../../plugins/http", features = [
"multipart", "multipart",
], version = "2.0.0-rc.0" } ], version = "2.0.0-rc.0" }
tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.0.0-rc.1", features = [ tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.0.0-rc.2", features = [
"windows7-compat", "windows7-compat",
] } ] }
tauri-plugin-os = { path = "../../../plugins/os", version = "2.0.0-rc.0" } tauri-plugin-os = { path = "../../../plugins/os", version = "2.0.0-rc.0" }
tauri-plugin-process = { path = "../../../plugins/process", version = "2.0.0-rc.0" } tauri-plugin-process = { path = "../../../plugins/process", version = "2.0.0-rc.0" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.0-rc.1" } tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.0-rc.2" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.0.0-rc.2" }
[dependencies.tauri] [dependencies.tauri]
workspace = true workspace = true
@ -54,9 +55,9 @@ tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", vers
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.0.0-rc.1" } tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.0.0-rc.1" }
[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies] [target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies]
tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.0.0-rc.1" } tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.0.0-rc.2" }
tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.0.0-rc.1" } tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.0.0-rc.2" }
tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.0.0-rc.1" } tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.0.0-rc.2" }
[target."cfg(target_os = \"windows\")".dependencies] [target."cfg(target_os = \"windows\")".dependencies]
window-shadows = "0.2" window-shadows = "0.2"

@ -61,6 +61,9 @@
"clipboard-manager:allow-write-text", "clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-image", "clipboard-manager:allow-read-image",
"clipboard-manager:allow-write-image", "clipboard-manager:allow-write-image",
"fs:allow-open",
"fs:allow-write",
"fs:allow-read",
"fs:allow-rename", "fs:allow-rename",
"fs:allow-mkdir", "fs:allow-mkdir",
"fs:allow-remove", "fs:allow-remove",
@ -75,6 +78,11 @@
} }
], ],
"deny": ["$APPDATA/db/*.stronghold"] "deny": ["$APPDATA/db/*.stronghold"]
} },
"store:allow-entries",
"store:allow-get",
"store:allow-set",
"store:allow-save",
"store:allow-load"
] ]
} }

@ -27,6 +27,7 @@
<option value="$PROJECT_DIR$/../../../../../plugins/biometric/android" /> <option value="$PROJECT_DIR$/../../../../../plugins/biometric/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/clipboard-manager/android" /> <option value="$PROJECT_DIR$/../../../../../plugins/clipboard-manager/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/dialog/android" /> <option value="$PROJECT_DIR$/../../../../../plugins/dialog/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/fs/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/nfc/android" /> <option value="$PROJECT_DIR$/../../../../../plugins/nfc/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/notification/android" /> <option value="$PROJECT_DIR$/../../../../../plugins/notification/android" />
<option value="$PROJECT_DIR$/../../../../../plugins/shell/android" /> <option value="$PROJECT_DIR$/../../../../../plugins/shell/android" />

@ -7265,6 +7265,181 @@
"shell:deny-stdin-write" "shell:deny-stdin-write"
] ]
}, },
{
"description": "store:default -> This permission set configures what kind of\noperations are available from the store plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string",
"enum": [
"store:default"
]
},
{
"description": "store:allow-clear -> Enables the clear command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-clear"
]
},
{
"description": "store:allow-delete -> Enables the delete command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-delete"
]
},
{
"description": "store:allow-entries -> Enables the entries command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-entries"
]
},
{
"description": "store:allow-get -> Enables the get command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-get"
]
},
{
"description": "store:allow-has -> Enables the has command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-has"
]
},
{
"description": "store:allow-keys -> Enables the keys command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-keys"
]
},
{
"description": "store:allow-length -> Enables the length command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-length"
]
},
{
"description": "store:allow-load -> Enables the load command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-load"
]
},
{
"description": "store:allow-reset -> Enables the reset command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-reset"
]
},
{
"description": "store:allow-save -> Enables the save command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-save"
]
},
{
"description": "store:allow-set -> Enables the set command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-set"
]
},
{
"description": "store:allow-values -> Enables the values command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-values"
]
},
{
"description": "store:deny-clear -> Denies the clear command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-clear"
]
},
{
"description": "store:deny-delete -> Denies the delete command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-delete"
]
},
{
"description": "store:deny-entries -> Denies the entries command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-entries"
]
},
{
"description": "store:deny-get -> Denies the get command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-get"
]
},
{
"description": "store:deny-has -> Denies the has command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-has"
]
},
{
"description": "store:deny-keys -> Denies the keys command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-keys"
]
},
{
"description": "store:deny-length -> Denies the length command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-length"
]
},
{
"description": "store:deny-load -> Denies the load command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-load"
]
},
{
"description": "store:deny-reset -> Denies the reset command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-reset"
]
},
{
"description": "store:deny-save -> Denies the save command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-save"
]
},
{
"description": "store:deny-set -> Denies the set command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-set"
]
},
{
"description": "store:deny-values -> Denies the values command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-values"
]
},
{ {
"description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n", "description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string", "type": "string",

@ -7341,6 +7341,181 @@
"enum": [ "enum": [
"shell:deny-stdin-write" "shell:deny-stdin-write"
] ]
},
{
"description": "store:default -> This permission set configures what kind of\noperations are available from the store plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string",
"enum": [
"store:default"
]
},
{
"description": "store:allow-clear -> Enables the clear command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-clear"
]
},
{
"description": "store:allow-delete -> Enables the delete command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-delete"
]
},
{
"description": "store:allow-entries -> Enables the entries command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-entries"
]
},
{
"description": "store:allow-get -> Enables the get command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-get"
]
},
{
"description": "store:allow-has -> Enables the has command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-has"
]
},
{
"description": "store:allow-keys -> Enables the keys command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-keys"
]
},
{
"description": "store:allow-length -> Enables the length command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-length"
]
},
{
"description": "store:allow-load -> Enables the load command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-load"
]
},
{
"description": "store:allow-reset -> Enables the reset command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-reset"
]
},
{
"description": "store:allow-save -> Enables the save command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-save"
]
},
{
"description": "store:allow-set -> Enables the set command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-set"
]
},
{
"description": "store:allow-values -> Enables the values command without any pre-configured scope.",
"type": "string",
"enum": [
"store:allow-values"
]
},
{
"description": "store:deny-clear -> Denies the clear command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-clear"
]
},
{
"description": "store:deny-delete -> Denies the delete command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-delete"
]
},
{
"description": "store:deny-entries -> Denies the entries command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-entries"
]
},
{
"description": "store:deny-get -> Denies the get command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-get"
]
},
{
"description": "store:deny-has -> Denies the has command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-has"
]
},
{
"description": "store:deny-keys -> Denies the keys command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-keys"
]
},
{
"description": "store:deny-length -> Denies the length command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-length"
]
},
{
"description": "store:deny-load -> Denies the load command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-load"
]
},
{
"description": "store:deny-reset -> Denies the reset command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-reset"
]
},
{
"description": "store:deny-save -> Denies the save command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-save"
]
},
{
"description": "store:deny-set -> Denies the set command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-set"
]
},
{
"description": "store:deny-values -> Denies the values command without any pre-configured scope.",
"type": "string",
"enum": [
"store:deny-values"
]
} }
] ]
}, },

@ -37,6 +37,7 @@ pub fn run() {
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_store::Builder::default().build())
.setup(move |app| { .setup(move |app| {
#[cfg(desktop)] #[cfg(desktop)]
{ {

@ -14,6 +14,7 @@
import Notifications from "./views/Notifications.svelte"; import Notifications from "./views/Notifications.svelte";
import Shortcuts from "./views/Shortcuts.svelte"; import Shortcuts from "./views/Shortcuts.svelte";
import Shell from "./views/Shell.svelte"; import Shell from "./views/Shell.svelte";
import Store from "./views/Store.svelte";
import Updater from "./views/Updater.svelte"; import Updater from "./views/Updater.svelte";
import Clipboard from "./views/Clipboard.svelte"; import Clipboard from "./views/Clipboard.svelte";
import WebRTC from "./views/WebRTC.svelte"; import WebRTC from "./views/WebRTC.svelte";
@ -90,6 +91,11 @@
component: Shell, component: Shell,
icon: "i-codicon-terminal-bash", icon: "i-codicon-terminal-bash",
}, },
{
label: "Store",
component: Store,
icon: "i-codicon-file-code",
},
!isMobile && { !isMobile && {
label: "Updater", label: "Updater",
component: Updater, component: Updater,

@ -23,11 +23,7 @@
async function prompt() { async function prompt() {
confirm("Do you want to do something?") confirm("Do you want to do something?")
.then((res) => .then((res) => onMessage(res ? "Yes" : "No"))
onMessage(
res ? "Yes" : "No"
)
)
.catch(onMessage); .catch(onMessage);
} }
@ -67,14 +63,15 @@
if (Array.isArray(res)) { if (Array.isArray(res)) {
onMessage(res); onMessage(res);
} else { } else {
var pathToRead = typeof res === "string" ? res : res.path; var pathToRead = res;
var isFile = pathToRead.match(/\S+\.\S+$/g); var isFile = pathToRead.match(/\S+\.\S+$/g);
readFile(pathToRead) readFile(pathToRead)
.then(function (response) { .then(function (response) {
if (isFile) { if (isFile) {
if ( if (
pathToRead.includes(".png") || pathToRead.includes(".png") ||
pathToRead.includes(".jpg") pathToRead.includes(".jpg") ||
pathToRead.includes(".jpeg")
) { ) {
arrayBufferToBase64( arrayBufferToBase64(
new Uint8Array(response), new Uint8Array(response),
@ -144,5 +141,7 @@
>Open save dialog</button >Open save dialog</button
> >
<button class="btn" id="prompt-dialog" on:click={prompt}>Prompt</button> <button class="btn" id="prompt-dialog" on:click={prompt}>Prompt</button>
<button class="btn" id="custom-prompt-dialog" on:click={promptCustom}>Prompt (custom)</button> <button class="btn" id="custom-prompt-dialog" on:click={promptCustom}
>Prompt (custom)</button
>
<button class="btn" id="message-dialog" on:click={msg}>Message</button> <button class="btn" id="message-dialog" on:click={msg}>Message</button>

@ -59,7 +59,7 @@
} }
function writeToStdin() { function writeToStdin() {
child.write(stdin).catch(onMessage); child.write(`${stdin}\n`).catch(onMessage);
} }
</script> </script>

@ -0,0 +1,55 @@
<script>
import { Store } from "@tauri-apps/plugin-store";
import { onMount } from "svelte";
export let onMessage;
let key;
let value;
const store = new Store("cache.json");
let cache = {};
onMount(async () => {
await store.load();
const values = await store.entries();
for (const [key, value] of values) {
cache[key] = value;
}
cache = cache;
});
function write(key, value) {
store
.set(key, value)
.then(() => store.get(key))
.then((v) => {
cache[key] = v;
cache = cache;
})
.then(() => store.save())
.catch(onMessage);
}
</script>
<div class="flex flex-col childre:grow gap-1">
<div class="flex flex-col flex-row-md gap-4">
<div class="flex items-center gap-1">
Key:
<input class="grow input" bind:value={key} />
</div>
<div class="flex items-center gap-1">
Value:
<input class="grow input" bind:value />
</div>
<button class="btn" on:click={() => write(key, value)}> Write </button>
</div>
<div>
{#each Object.entries(cache) as [k, v]}
<div>{k} = {v}</div>
{/each}
</div>
</div>

@ -1,5 +1,9 @@
# Changelog # Changelog
## \[2.0.0-rc.2]
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
## \[2.0.0-rc.1] ## \[2.0.0-rc.1]
- [`2c00c029`](https://github.com/tauri-apps/plugins-workspace/commit/2c00c0292c9127b81567de46691e8c0f73557261) ([#1630](https://github.com/tauri-apps/plugins-workspace/pull/1630) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused multi-word IIFE names to not be formatted correctly. For example the `barcode-scanner` was defined as `window.__TAURI_PLUGIN_CLIPBOARDMANAGER__` instead of `window.__TAURI_PLUGIN_CLIPBOARD_MANAGER__`. - [`2c00c029`](https://github.com/tauri-apps/plugins-workspace/commit/2c00c0292c9127b81567de46691e8c0f73557261) ([#1630](https://github.com/tauri-apps/plugins-workspace/pull/1630) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused multi-word IIFE names to not be formatted correctly. For example the `barcode-scanner` was defined as `window.__TAURI_PLUGIN_CLIPBOARDMANAGER__` instead of `window.__TAURI_PLUGIN_CLIPBOARD_MANAGER__`.

@ -1,6 +1,6 @@
[package] [package]
name = "tauri-plugin-barcode-scanner" name = "tauri-plugin-barcode-scanner"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
description = "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS" description = "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS"
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }

@ -10,7 +10,8 @@ import PackageDescription
let package = Package( let package = Package(
name: "tauri-plugin-barcode-scanner", name: "tauri-plugin-barcode-scanner",
platforms: [ platforms: [
.iOS(.v13) .macOS(.v10_13),
.iOS(.v13),
], ],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.

@ -1,5 +1,9 @@
# Changelog # Changelog
## \[2.0.0-rc.2]
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
## \[2.0.0-rc.1] ## \[2.0.0-rc.1]
### changes ### changes

@ -1,6 +1,6 @@
[package] [package]
name = "tauri-plugin-biometric" name = "tauri-plugin-biometric"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
description = "Prompt the user for biometric authentication on Android and iOS." description = "Prompt the user for biometric authentication on Android and iOS."
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }

@ -8,7 +8,8 @@ import PackageDescription
let package = Package( let package = Package(
name: "tauri-plugin-biometric", name: "tauri-plugin-biometric",
platforms: [ platforms: [
.iOS(.v13) .macOS(.v10_13),
.iOS(.v13),
], ],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.

@ -1,5 +1,9 @@
# Changelog # Changelog
## \[2.0.0-rc.2]
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
## \[2.0.0-rc.1] ## \[2.0.0-rc.1]
- [`2c00c029`](https://github.com/tauri-apps/plugins-workspace/commit/2c00c0292c9127b81567de46691e8c0f73557261) ([#1630](https://github.com/tauri-apps/plugins-workspace/pull/1630) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused multi-word IIFE names to not be formatted correctly. For example the `barcode-scanner` was defined as `window.__TAURI_PLUGIN_CLIPBOARDMANAGER__` instead of `window.__TAURI_PLUGIN_CLIPBOARD_MANAGER__`. - [`2c00c029`](https://github.com/tauri-apps/plugins-workspace/commit/2c00c0292c9127b81567de46691e8c0f73557261) ([#1630](https://github.com/tauri-apps/plugins-workspace/pull/1630) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused multi-word IIFE names to not be formatted correctly. For example the `barcode-scanner` was defined as `window.__TAURI_PLUGIN_CLIPBOARDMANAGER__` instead of `window.__TAURI_PLUGIN_CLIPBOARD_MANAGER__`.

@ -1,6 +1,6 @@
[package] [package]
name = "tauri-plugin-clipboard-manager" name = "tauri-plugin-clipboard-manager"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
description = "Read and write to the system clipboard." description = "Read and write to the system clipboard."
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }

@ -6,28 +6,29 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "tauri-plugin-clipboard-manager", name: "tauri-plugin-clipboard-manager",
platforms: [ platforms: [
.iOS(.v13), .macOS(.v10_13),
], .iOS(.v13),
products: [ ],
// Products define the executables and libraries a package produces, and make them visible to other packages. products: [
.library( // Products define the executables and libraries a package produces, and make them visible to other packages.
name: "tauri-plugin-clipboard-manager", .library(
type: .static, name: "tauri-plugin-clipboard-manager",
targets: ["tauri-plugin-clipboard-manager"]), type: .static,
], targets: ["tauri-plugin-clipboard-manager"])
dependencies: [ ],
.package(name: "Tauri", path: "../.tauri/tauri-api") dependencies: [
], .package(name: "Tauri", path: "../.tauri/tauri-api")
targets: [ ],
// Targets are the basic building blocks of a package. A target can define a module or a test suite. targets: [
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets are the basic building blocks of a package. A target can define a module or a test suite.
.target( // Targets can depend on other targets in this package, and on products in packages this package depends on.
name: "tauri-plugin-clipboard-manager", .target(
dependencies: [ name: "tauri-plugin-clipboard-manager",
.byName(name: "Tauri") dependencies: [
], .byName(name: "Tauri")
path: "Sources") ],
] path: "Sources")
]
) )

@ -14,7 +14,7 @@
"@tauri-apps/plugin-deep-link": "2.0.0-rc.0" "@tauri-apps/plugin-deep-link": "2.0.0-rc.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "2.0.0-rc.4", "@tauri-apps/cli": "2.0.0-rc.6",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.0.13" "vite": "^5.0.13"
} }

@ -1,5 +1,9 @@
# Changelog # Changelog
## \[2.0.0-rc.2]
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
## \[2.0.0-rc.1] ## \[2.0.0-rc.1]
### feat ### feat

@ -1,6 +1,6 @@
[package] [package]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application." description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application."
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
@ -17,6 +17,9 @@ targets = ["x86_64-unknown-linux-gnu", "x86_64-linux-android"]
[build-dependencies] [build-dependencies]
tauri-plugin = { workspace = true, features = [ "build" ] } tauri-plugin = { workspace = true, features = [ "build" ] }
[dev-dependencies]
tauri = { workspace = true, features = [ "wry" ] }
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
@ -24,6 +27,7 @@ tauri = { workspace = true }
log = { workspace = true } log = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
dunce = { workspace = true } dunce = { workspace = true }
url = { workspace = true }
tauri-plugin-fs = { path = "../fs", version = "2.0.0-rc.0" } tauri-plugin-fs = { path = "../fs", version = "2.0.0-rc.0" }
[target.'cfg(target_os = "ios")'.dependencies] [target.'cfg(target_os = "ios")'.dependencies]

@ -30,7 +30,6 @@ class Filter {
class FilePickerOptions { class FilePickerOptions {
lateinit var filters: Array<Filter> lateinit var filters: Array<Filter>
var multiple: Boolean? = null var multiple: Boolean? = null
var readData: Boolean? = null
} }
@InvokeArg @InvokeArg
@ -95,7 +94,7 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
try { try {
when (result.resultCode) { when (result.resultCode) {
Activity.RESULT_OK -> { Activity.RESULT_OK -> {
val callResult = createPickFilesResult(result.data, filePickerOptions?.readData ?: false) val callResult = createPickFilesResult(result.data)
invoke.resolve(callResult) invoke.resolve(callResult)
} }
Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled") Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled")
@ -108,49 +107,23 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
} }
} }
private fun createPickFilesResult(data: Intent?, readData: Boolean): JSObject { private fun createPickFilesResult(data: Intent?): JSObject {
val callResult = JSObject() val callResult = JSObject()
val filesResultList: MutableList<JSObject> = ArrayList()
if (data == null) { if (data == null) {
callResult.put("files", JSArray.from(filesResultList)) callResult.put("files", null)
return callResult return callResult
} }
val uris: MutableList<Uri?> = ArrayList() val uris: MutableList<String?> = ArrayList()
if (data.clipData == null) { if (data.clipData == null) {
val uri: Uri? = data.data val uri: Uri? = data.data
uris.add(uri) uris.add(uri?.toString())
} else { } else {
for (i in 0 until data.clipData!!.itemCount) { for (i in 0 until data.clipData!!.itemCount) {
val uri: Uri = data.clipData!!.getItemAt(i).uri val uri: Uri = data.clipData!!.getItemAt(i).uri
uris.add(uri) uris.add(uri.toString())
} }
} }
for (i in uris.indices) { callResult.put("files", JSArray.from(uris.toTypedArray()))
val uri = uris[i] ?: continue
val fileResult = JSObject()
if (readData) {
fileResult.put("base64Data", FilePickerUtils.getDataFromUri(activity, uri))
}
val duration = FilePickerUtils.getDurationFromUri(activity, uri)
if (duration != null) {
fileResult.put("duration", duration)
}
val resolution = FilePickerUtils.getHeightAndWidthFromUri(activity, uri)
if (resolution != null) {
fileResult.put("height", resolution.height)
fileResult.put("width", resolution.width)
}
fileResult.put("mimeType", FilePickerUtils.getMimeTypeFromUri(activity, uri))
val modifiedAt = FilePickerUtils.getModifiedAtFromUri(activity, uri)
if (modifiedAt != null) {
fileResult.put("modifiedAt", modifiedAt)
}
fileResult.put("name", FilePickerUtils.getNameFromUri(activity, uri))
fileResult.put("path", FilePickerUtils.getPathFromUri(activity, uri))
fileResult.put("size", FilePickerUtils.getSizeFromUri(activity, uri))
filesResultList.add(fileResult)
}
callResult.put("files", JSArray.from(filesResultList.toTypedArray()))
return callResult return callResult
} }

@ -4,18 +4,6 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
interface FileResponse {
base64Data?: string;
duration?: number;
height?: number;
width?: number;
mimeType?: string;
modifiedAt?: number;
name?: string;
path: string;
size: number;
}
/** /**
* Extension filters for the file dialog. * Extension filters for the file dialog.
* *
@ -117,8 +105,8 @@ type OpenDialogReturn<T extends OpenDialogOptions> = T["directory"] extends true
? string[] | null ? string[] | null
: string | null : string | null
: T["multiple"] extends true : T["multiple"] extends true
? FileResponse[] | null ? string[] | null
: FileResponse | null; : string | null;
/** /**
* Open a file/directory selection dialog. * Open a file/directory selection dialog.
@ -306,7 +294,6 @@ async function confirm(
export type { export type {
DialogFilter, DialogFilter,
FileResponse,
OpenDialogOptions, OpenDialogOptions,
OpenDialogReturn, OpenDialogReturn,
SaveDialogOptions, SaveDialogOptions,

@ -6,28 +6,29 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "tauri-plugin-dialog", name: "tauri-plugin-dialog",
platforms: [ platforms: [
.iOS(.v13), .macOS(.v10_13),
], .iOS(.v13),
products: [ ],
// Products define the executables and libraries a package produces, and make them visible to other packages. products: [
.library( // Products define the executables and libraries a package produces, and make them visible to other packages.
name: "tauri-plugin-dialog", .library(
type: .static, name: "tauri-plugin-dialog",
targets: ["tauri-plugin-dialog"]), type: .static,
], targets: ["tauri-plugin-dialog"])
dependencies: [ ],
.package(name: "Tauri", path: "../.tauri/tauri-api") dependencies: [
], .package(name: "Tauri", path: "../.tauri/tauri-api")
targets: [ ],
// Targets are the basic building blocks of a package. A target can define a module or a test suite. targets: [
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets are the basic building blocks of a package. A target can define a module or a test suite.
.target( // Targets can depend on other targets in this package, and on products in packages this package depends on.
name: "tauri-plugin-dialog", .target(
dependencies: [ name: "tauri-plugin-dialog",
.byName(name: "Tauri") dependencies: [
], .byName(name: "Tauri")
path: "Sources") ],
] path: "Sources")
]
) )

@ -29,7 +29,6 @@ struct Filter: Decodable {
struct FilePickerOptions: Decodable { struct FilePickerOptions: Decodable {
var multiple: Bool? var multiple: Bool?
var readData: Bool?
var filters: [Filter]? var filters: [Filter]?
} }
@ -136,55 +135,9 @@ class DialogPlugin: Plugin {
public func onFilePickerEvent(_ event: FilePickerEvent) { public func onFilePickerEvent(_ event: FilePickerEvent) {
switch event { switch event {
case .selected(let urls): case .selected(let urls):
let readData = pendingInvokeArgs?.readData ?? false
do {
let filesResult = try urls.map { (url: URL) -> JSObject in
var file = JSObject()
let mimeType = filePickerController.getMimeTypeFromUrl(url)
let isVideo = mimeType.hasPrefix("video")
let isImage = mimeType.hasPrefix("image")
if readData {
file["data"] = try Data(contentsOf: url).base64EncodedString()
}
if isVideo {
file["duration"] = filePickerController.getVideoDuration(url)
let (height, width) = filePickerController.getVideoDimensions(url)
if let height = height {
file["height"] = height
}
if let width = width {
file["width"] = width
}
} else if isImage {
let (height, width) = filePickerController.getImageDimensions(url)
if let height = height {
file["height"] = height
}
if let width = width {
file["width"] = width
}
}
file["modifiedAt"] = filePickerController.getModifiedAtFromUrl(url)
file["mimeType"] = mimeType
file["name"] = url.lastPathComponent
file["path"] = url.absoluteString
file["size"] = try filePickerController.getSizeFromUrl(url)
return file
}
pendingInvoke?.resolve(["files": filesResult])
} catch let error as NSError {
pendingInvoke?.reject(error.localizedDescription, error: error)
return
}
pendingInvoke?.resolve(["files": urls]) pendingInvoke?.resolve(["files": urls])
case .cancelled: case .cancelled:
let files: JSArray = [] pendingInvoke?.resolve(["files": nil])
pendingInvoke?.resolve(["files": files])
case .error(let error): case .error(let error):
pendingInvoke?.reject(error) pendingInvoke?.reject(error)
} }

@ -8,17 +8,17 @@ use serde::{Deserialize, Serialize};
use tauri::{command, Manager, Runtime, State, Window}; use tauri::{command, Manager, Runtime, State, Window};
use tauri_plugin_fs::FsExt; use tauri_plugin_fs::FsExt;
use crate::{Dialog, FileDialogBuilder, FileResponse, MessageDialogKind, Result}; use crate::{Dialog, FileDialogBuilder, FilePath, MessageDialogKind, Result};
#[derive(Serialize)] #[derive(Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum OpenResponse { pub enum OpenResponse {
#[cfg(desktop)] #[cfg(desktop)]
Folders(Option<Vec<PathBuf>>), Folders(Option<Vec<FilePath>>),
#[cfg(desktop)] #[cfg(desktop)]
Folder(Option<PathBuf>), Folder(Option<FilePath>),
Files(Option<Vec<FileResponse>>), Files(Option<Vec<FilePath>>),
File(Option<FileResponse>), File(Option<FilePath>),
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -136,25 +136,26 @@ pub(crate) async fn open<R: Runtime>(
let folders = dialog_builder.blocking_pick_folders(); let folders = dialog_builder.blocking_pick_folders();
if let Some(folders) = &folders { if let Some(folders) = &folders {
for folder in folders { for folder in folders {
if let Some(s) = window.try_fs_scope() { if let Ok(path) = folder.path() {
s.allow_directory(folder, options.recursive); if let Some(s) = window.try_fs_scope() {
s.allow_directory(path, options.recursive);
}
} }
} }
} }
OpenResponse::Folders(folders.map(|folders| { OpenResponse::Folders(
folders folders.map(|folders| folders.into_iter().map(|p| p.simplified()).collect()),
.iter() )
.map(|p| dunce::simplified(p).to_path_buf())
.collect()
}))
} else { } else {
let folder = dialog_builder.blocking_pick_folder(); let folder = dialog_builder.blocking_pick_folder();
if let Some(path) = &folder { if let Some(folder) = &folder {
if let Some(s) = window.try_fs_scope() { if let Ok(path) = folder.path() {
s.allow_directory(path, options.recursive); if let Some(s) = window.try_fs_scope() {
s.allow_directory(path, options.recursive);
}
} }
} }
OpenResponse::Folder(folder.map(|p| dunce::simplified(&p).to_path_buf())) OpenResponse::Folder(folder.map(|p| p.simplified()))
} }
} }
#[cfg(mobile)] #[cfg(mobile)]
@ -163,37 +164,28 @@ pub(crate) async fn open<R: Runtime>(
let files = dialog_builder.blocking_pick_files(); let files = dialog_builder.blocking_pick_files();
if let Some(files) = &files { if let Some(files) = &files {
for file in files { for file in files {
if let Some(s) = window.try_fs_scope() { if let Ok(path) = file.path() {
s.allow_file(&file.path); if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
}
window.state::<tauri::scope::Scopes>().allow_file(&path)?;
} }
window
.state::<tauri::scope::Scopes>()
.allow_file(&file.path)?;
} }
} }
OpenResponse::Files(files.map(|files| { OpenResponse::Files(files.map(|files| files.into_iter().map(|f| f.simplified()).collect()))
files
.into_iter()
.map(|mut f| {
f.path = dunce::simplified(&f.path).to_path_buf();
f
})
.collect()
}))
} else { } else {
let file = dialog_builder.blocking_pick_file(); let file = dialog_builder.blocking_pick_file();
if let Some(file) = &file { if let Some(file) = &file {
if let Some(s) = window.try_fs_scope() { if let Ok(path) = file.path() {
s.allow_file(&file.path); if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
}
window.state::<tauri::scope::Scopes>().allow_file(&path)?;
} }
window
.state::<tauri::scope::Scopes>()
.allow_file(&file.path)?;
} }
OpenResponse::File(file.map(|mut f| { OpenResponse::File(file.map(|f| f.simplified()))
f.path = dunce::simplified(&f.path).to_path_buf();
f
}))
}; };
Ok(res) Ok(res)
} }
@ -204,7 +196,7 @@ pub(crate) async fn save<R: Runtime>(
window: Window<R>, window: Window<R>,
dialog: State<'_, Dialog<R>>, dialog: State<'_, Dialog<R>>,
options: SaveDialogOptions, options: SaveDialogOptions,
) -> Result<Option<PathBuf>> { ) -> Result<Option<FilePath>> {
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
return Err(crate::Error::FileSaveDialogNotImplemented); return Err(crate::Error::FileSaveDialogNotImplemented);
#[cfg(any(desktop, target_os = "android"))] #[cfg(any(desktop, target_os = "android"))]
@ -230,13 +222,15 @@ pub(crate) async fn save<R: Runtime>(
let path = dialog_builder.blocking_save_file(); let path = dialog_builder.blocking_save_file();
if let Some(p) = &path { if let Some(p) = &path {
if let Some(s) = window.try_fs_scope() { if let Ok(path) = p.path() {
s.allow_file(p); if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
}
window.state::<tauri::scope::Scopes>().allow_file(&path)?;
} }
window.state::<tauri::scope::Scopes>().allow_file(p)?;
} }
Ok(path.map(|p| dunce::simplified(&p).to_path_buf())) Ok(path.map(|p| p.simplified()))
} }
} }

@ -8,14 +8,12 @@
//! to give results back. This is particularly useful when running dialogs from the main thread. //! to give results back. This is particularly useful when running dialogs from the main thread.
//! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended. //! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended.
use std::path::PathBuf;
use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use rfd::{AsyncFileDialog, AsyncMessageDialog}; use rfd::{AsyncFileDialog, AsyncMessageDialog};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime}; use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::{models::*, FileDialogBuilder, MessageDialogBuilder}; use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder};
const OK: &str = "Ok"; const OK: &str = "Ok";
@ -115,11 +113,11 @@ impl<R: Runtime> From<MessageDialogBuilder<R>> for AsyncMessageDialog {
} }
} }
pub fn pick_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>( pub fn pick_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
) { ) {
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf())); let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
let handle = dialog.dialog.app_handle().to_owned(); let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || { let _ = handle.run_on_main_thread(move || {
let dialog = AsyncFileDialog::from(dialog).pick_file(); let dialog = AsyncFileDialog::from(dialog).pick_file();
@ -127,12 +125,16 @@ pub fn pick_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
}); });
} }
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>( pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
) { ) {
let f = |paths: Option<Vec<rfd::FileHandle>>| { let f = |paths: Option<Vec<rfd::FileHandle>>| {
f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect())) f(paths.map(|list| {
list.into_iter()
.map(|p| p.path().to_path_buf().into())
.collect()
}))
}; };
let handle = dialog.dialog.app_handle().to_owned(); let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || { let _ = handle.run_on_main_thread(move || {
@ -141,11 +143,11 @@ pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
}); });
} }
pub fn pick_folder<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>( pub fn pick_folder<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
) { ) {
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf())); let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
let handle = dialog.dialog.app_handle().to_owned(); let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || { let _ = handle.run_on_main_thread(move || {
let dialog = AsyncFileDialog::from(dialog).pick_folder(); let dialog = AsyncFileDialog::from(dialog).pick_folder();
@ -153,12 +155,16 @@ pub fn pick_folder<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
}); });
} }
pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>( pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
) { ) {
let f = |paths: Option<Vec<rfd::FileHandle>>| { let f = |paths: Option<Vec<rfd::FileHandle>>| {
f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect())) f(paths.map(|list| {
list.into_iter()
.map(|p| p.path().to_path_buf().into())
.collect()
}))
}; };
let handle = dialog.dialog.app_handle().to_owned(); let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || { let _ = handle.run_on_main_thread(move || {
@ -167,11 +173,11 @@ pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static
}); });
} }
pub fn save_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>( pub fn save_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
) { ) {
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf())); let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
let handle = dialog.dialog.app_handle().to_owned(); let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || { let _ = handle.run_on_main_thread(move || {
let dialog = AsyncFileDialog::from(dialog).save_file(); let dialog = AsyncFileDialog::from(dialog).save_file();

@ -23,6 +23,8 @@ pub enum Error {
FileSaveDialogNotImplemented, FileSaveDialogNotImplemented,
#[error(transparent)] #[error(transparent)]
Fs(#[from] tauri_plugin_fs::Error), Fs(#[from] tauri_plugin_fs::Error),
#[error("URL is not a valid path")]
InvalidPathUrl,
} }
impl Serialize for Error { impl Serialize for Error {

@ -17,9 +17,6 @@ use tauri::{
Manager, Runtime, Manager, Runtime,
}; };
#[cfg(any(desktop, target_os = "ios"))]
use std::fs;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::mpsc::sync_channel, sync::mpsc::sync_channel,
@ -66,6 +63,87 @@ impl<R: Runtime, T: Manager<R>> crate::DialogExt<R> for T {
} }
impl<R: Runtime> Dialog<R> { impl<R: Runtime> Dialog<R> {
/// Create a new messaging dialog builder.
/// The dialog can optionally ask the user for confirmation or include an OK button.
///
/// # Examples
///
/// - Message dialog:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app
/// .dialog()
/// .message("Tauri is Awesome!")
/// .show(|_| {
/// println!("dialog closed");
/// });
/// Ok(())
/// });
/// ```
///
/// - Ask dialog:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app.dialog()
/// .message("Are you sure?")
/// .ok_button_label("Yes")
/// .cancel_button_label("No")
/// .show(|yes| {
/// println!("user said {}", if yes { "yes" } else { "no" });
/// });
/// Ok(())
/// });
/// ```
///
/// - Message dialog with OK button:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app.dialog()
/// .message("Job completed successfully")
/// .ok_button_label("Ok")
/// .show(|_| {
/// println!("dialog closed");
/// });
/// Ok(())
/// });
/// ```
///
/// # `show` vs `blocking_show`
///
/// The dialog builder includes two separate APIs for rendering the dialog: `show` and `blocking_show`.
/// The `show` function is asynchronous and takes a closure to be executed when the dialog is closed.
/// To block the current thread until the user acted on the dialog, you can use `blocking_show`,
/// but note that it cannot be executed on the main thread as it will freeze your application.
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// let handle = app.handle().clone();
/// std::thread::spawn(move || {
/// let yes = handle.dialog()
/// .message("Are you sure?")
/// .ok_button_label("Yes")
/// .cancel_button_label("No")
/// .blocking_show();
/// });
///
/// Ok(())
/// });
/// ```
pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> { pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> {
MessageDialogBuilder::new( MessageDialogBuilder::new(
self.clone(), self.clone(),
@ -74,6 +152,7 @@ impl<R: Runtime> Dialog<R> {
) )
} }
/// Creates a new builder for dialogs that lets ths user select file(s) or folder(s).
pub fn file(&self) -> FileDialogBuilder<R> { pub fn file(&self) -> FileDialogBuilder<R> {
FileDialogBuilder::new(self.clone()) FileDialogBuilder::new(self.clone())
} }
@ -216,35 +295,52 @@ impl<R: Runtime> MessageDialogBuilder<R> {
} }
} }
#[derive(Debug, Deserialize, Serialize, Default)] /// Represents either a filesystem path or a URI pointing to a file
#[serde(rename_all = "camelCase")] /// such as `file://` URIs or Android `content://` URIs.
pub struct FileResponse { #[derive(Debug, Deserialize, Serialize)]
pub base64_data: Option<String>, #[serde(untagged)]
pub duration: Option<u64>, pub enum FilePath {
pub height: Option<usize>, Url(url::Url),
pub width: Option<usize>, Path(PathBuf),
pub mime_type: Option<String>,
pub modified_at: Option<u64>,
pub name: Option<String>,
pub path: PathBuf,
pub size: u64,
} }
impl FileResponse { impl From<PathBuf> for FilePath {
#[cfg(desktop)] fn from(value: PathBuf) -> Self {
fn new(path: PathBuf) -> Self { Self::Path(value)
let metadata = fs::metadata(&path); }
let metadata = metadata.as_ref(); }
Self {
base64_data: None, impl From<url::Url> for FilePath {
duration: None, fn from(value: url::Url) -> Self {
height: None, Self::Url(value)
width: None, }
mime_type: None, }
modified_at: metadata.ok().and_then(|m| to_msec(m.modified())),
name: path.file_name().map(|f| f.to_string_lossy().into_owned()), impl From<FilePath> for tauri_plugin_fs::FilePath {
path, fn from(value: FilePath) -> Self {
size: metadata.map(|m| m.len()).unwrap_or(0), match value {
FilePath::Path(p) => tauri_plugin_fs::FilePath::Path(p),
FilePath::Url(url) => tauri_plugin_fs::FilePath::Url(url),
}
}
}
impl FilePath {
fn simplified(self) -> Self {
match self {
Self::Url(url) => Self::Url(url),
Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()),
}
}
#[inline]
fn path(&self) -> Result<PathBuf> {
match self {
Self::Url(url) => url
.to_file_path()
.map(PathBuf::from)
.map_err(|_| Error::InvalidPathUrl),
Self::Path(p) => Ok(p.to_owned()),
} }
} }
} }
@ -362,21 +458,18 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default() /// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json")) /// .setup(|app| {
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// app.dialog().file().pick_file(|file_path| { /// app.dialog().file().pick_file(|file_path| {
/// // do something with the optional file path here /// // do something with the optional file path here
/// // the file path is `None` if the user closed the dialog /// // the file path is `None` if the user closed the dialog
/// }) /// });
/// }) /// Ok(())
/// });
/// ``` /// ```
pub fn pick_file<F: FnOnce(Option<FileResponse>) + Send + 'static>(self, f: F) { pub fn pick_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
#[cfg(desktop)]
let f = |path: Option<PathBuf>| f(path.map(FileResponse::new));
pick_file(self, f) pick_file(self, f)
} }
@ -384,29 +477,44 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// This is not a blocking operation, /// This is not a blocking operation,
/// and should be used when running on the main thread to avoid deadlocks with the event loop. /// and should be used when running on the main thread to avoid deadlocks with the event loop.
/// ///
/// # Reading the files
///
/// The file paths cannot be read directly on Android as they are behind a content URI.
/// The recommended way to read the files is using the [`fs`](https://v2.tauri.app/plugin/file-system/) plugin:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// use tauri_plugin_fs::FsExt;
/// tauri::Builder::default()
/// .setup(|app| {
/// let handle = app.handle().clone();
/// app.dialog().file().pick_file(move |file_path| {
/// let Some(path) = file_path else { return };
/// let Ok(contents) = handle.fs().read_to_string(path) else {
/// eprintln!("failed to read file, <todo add error handling!>");
/// return;
/// };
/// });
/// Ok(())
/// });
/// ```
///
/// See <https://developer.android.com/guide/topics/providers/content-provider-basics> for more information.
///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default() /// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json")) /// .setup(|app| {
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// app.dialog().file().pick_files(|file_paths| { /// app.dialog().file().pick_files(|file_paths| {
/// // do something with the optional file paths here /// // do something with the optional file paths here
/// // the file paths value is `None` if the user closed the dialog /// // the file paths value is `None` if the user closed the dialog
/// }) /// });
/// }) /// Ok(())
/// });
/// ``` /// ```
pub fn pick_files<F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(self, f: F) { pub fn pick_files<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
#[cfg(desktop)]
let f = |paths: Option<Vec<PathBuf>>| {
f(paths.map(|p| {
p.into_iter()
.map(FileResponse::new)
.collect::<Vec<FileResponse>>()
}))
};
pick_files(self, f) pick_files(self, f)
} }
@ -416,20 +524,19 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default() /// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json")) /// .setup(|app| {
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// app.dialog().file().pick_folder(|folder_path| { /// app.dialog().file().pick_folder(|folder_path| {
/// // do something with the optional folder path here /// // do something with the optional folder path here
/// // the folder path is `None` if the user closed the dialog /// // the folder path is `None` if the user closed the dialog
/// }) /// });
/// }) /// Ok(())
/// });
/// ``` /// ```
#[cfg(desktop)] #[cfg(desktop)]
pub fn pick_folder<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) { pub fn pick_folder<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_folder(self, f) pick_folder(self, f)
} }
@ -439,20 +546,19 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default() /// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json")) /// .setup(|app| {
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// app.dialog().file().pick_folders(|file_paths| { /// app.dialog().file().pick_folders(|file_paths| {
/// // do something with the optional folder paths here /// // do something with the optional folder paths here
/// // the folder paths value is `None` if the user closed the dialog /// // the folder paths value is `None` if the user closed the dialog
/// }) /// });
/// }) /// Ok(())
/// });
/// ``` /// ```
#[cfg(desktop)] #[cfg(desktop)]
pub fn pick_folders<F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(self, f: F) { pub fn pick_folders<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
pick_folders(self, f) pick_folders(self, f)
} }
@ -463,19 +569,18 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default() /// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json")) /// .setup(|app| {
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// app.dialog().file().save_file(|file_path| { /// app.dialog().file().save_file(|file_path| {
/// // do something with the optional file path here /// // do something with the optional file path here
/// // the file path is `None` if the user closed the dialog /// // the file path is `None` if the user closed the dialog
/// }) /// });
/// }) /// Ok(())
/// });
/// ``` /// ```
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) { pub fn save_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
save_file(self, f) save_file(self, f)
} }
} }
@ -488,7 +593,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command] /// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) { /// async fn my_command(app: tauri::AppHandle) {
@ -497,7 +602,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog /// // the file path is `None` if the user closed the dialog
/// } /// }
/// ``` /// ```
pub fn blocking_pick_file(self) -> Option<FileResponse> { pub fn blocking_pick_file(self) -> Option<FilePath> {
blocking_fn!(self, pick_file) blocking_fn!(self, pick_file)
} }
@ -507,7 +612,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command] /// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) { /// async fn my_command(app: tauri::AppHandle) {
@ -516,7 +621,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file paths value is `None` if the user closed the dialog /// // the file paths value is `None` if the user closed the dialog
/// } /// }
/// ``` /// ```
pub fn blocking_pick_files(self) -> Option<Vec<FileResponse>> { pub fn blocking_pick_files(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_files) blocking_fn!(self, pick_files)
} }
@ -526,7 +631,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command] /// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) { /// async fn my_command(app: tauri::AppHandle) {
@ -536,7 +641,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// } /// }
/// ``` /// ```
#[cfg(desktop)] #[cfg(desktop)]
pub fn blocking_pick_folder(self) -> Option<PathBuf> { pub fn blocking_pick_folder(self) -> Option<FilePath> {
blocking_fn!(self, pick_folder) blocking_fn!(self, pick_folder)
} }
@ -546,7 +651,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command] /// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) { /// async fn my_command(app: tauri::AppHandle) {
@ -556,7 +661,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// } /// }
/// ``` /// ```
#[cfg(desktop)] #[cfg(desktop)]
pub fn blocking_pick_folders(self) -> Option<Vec<PathBuf>> { pub fn blocking_pick_folders(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_folders) blocking_fn!(self, pick_folders)
} }
@ -566,7 +671,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust,no_run /// ```
/// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command] /// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) { /// async fn my_command(app: tauri::AppHandle) {
@ -575,23 +680,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog /// // the file path is `None` if the user closed the dialog
/// } /// }
/// ``` /// ```
pub fn blocking_save_file(self) -> Option<PathBuf> { pub fn blocking_save_file(self) -> Option<FilePath> {
blocking_fn!(self, save_file) blocking_fn!(self, save_file)
} }
} }
// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913
#[cfg(desktop)]
#[inline]
fn to_msec(maybe_time: std::result::Result<std::time::SystemTime, std::io::Error>) -> Option<u64> {
match maybe_time {
Ok(time) => {
let msec = time
.duration_since(std::time::UNIX_EPOCH)
.map(|t| t.as_millis() as u64)
.unwrap_or_else(|err| err.duration().as_millis() as u64);
Some(msec)
}
Err(_) => None,
}
}

@ -1,7 +1,6 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::path::PathBuf;
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
use tauri::{ use tauri::{
@ -9,7 +8,7 @@ use tauri::{
AppHandle, Runtime, AppHandle, Runtime,
}; };
use crate::{FileDialogBuilder, FileResponse, MessageDialogBuilder}; use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder};
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog"; const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog";
@ -47,15 +46,15 @@ impl<R: Runtime> Dialog<R> {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct FilePickerResponse { struct FilePickerResponse {
files: Vec<FileResponse>, files: Vec<FilePath>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct SaveFileResponse { struct SaveFileResponse {
file: PathBuf, file: FilePath,
} }
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>( pub fn pick_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
) { ) {
@ -72,7 +71,7 @@ pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
}); });
} }
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>( pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
) { ) {
@ -89,7 +88,7 @@ pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FileResponse>>) + Send + 'sta
}); });
} }
pub fn save_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>( pub fn save_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>, dialog: FileDialogBuilder<R>,
f: F, f: F,
) { ) {

@ -4,11 +4,12 @@ plugins {
} }
android { android {
namespace = "app.tauri.store" namespace = "com.plugin.fs"
compileSdk = 34 compileSdk = 34
defaultConfig { defaultConfig {
minSdk = 24 minSdk = 21
targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
@ -33,7 +34,12 @@ android {
} }
dependencies { dependencies {
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation(project(":tauri-android")) implementation(project(":tauri-android"))
} }

@ -0,0 +1,31 @@
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
google()
}
resolutionStrategy {
eachPlugin {
switch (requested.id.id) {
case "com.android.library":
useVersion("8.0.2")
break
case "org.jetbrains.kotlin.android":
useVersion("1.8.20")
break
}
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
google()
}
}
include ':tauri-android'
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')

@ -0,0 +1,28 @@
// 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)
}
}

@ -0,0 +1,93 @@
// 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)
}
}
}
}

@ -0,0 +1,21 @@
// 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)
}
}

@ -111,6 +111,8 @@ fn main() {
identifier = "scope-{lower}-recursive" identifier = "scope-{lower}-recursive"
description = "This scope permits recursive access to the complete `${upper}` folder, including sub directories and files." description = "This scope permits recursive access to the complete `${upper}` folder, including sub directories and files."
[[permission.scope.allow]]
path = "${upper}"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "${upper}/**" path = "${upper}/**"
@ -118,6 +120,8 @@ path = "${upper}/**"
identifier = "scope-{lower}" identifier = "scope-{lower}"
description = "This scope permits access to all files and list content of top level directories in the `${upper}`folder." description = "This scope permits access to all files and list content of top level directories in the `${upper}`folder."
[[permission.scope.allow]]
path = "${upper}"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "${upper}/*" path = "${upper}/*"
@ -126,7 +130,7 @@ identifier = "scope-{lower}-index"
description = "This scope permits to list all files and folders in the `${upper}`folder." description = "This scope permits to list all files and folders in the `${upper}`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "${upper}/" path = "${upper}"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands
@ -190,5 +194,6 @@ permissions = [
tauri_plugin::Builder::new(COMMANDS) tauri_plugin::Builder::new(COMMANDS)
.global_api_script_path("./api-iife.js") .global_api_script_path("./api-iife.js")
.global_scope_schema(schemars::schema_for!(FsScopeEntry)) .global_scope_schema(schemars::schema_for!(FsScopeEntry))
.android_path("android")
.build(); .build();
} }

@ -9,6 +9,8 @@
identifier = "scope-app-recursive" identifier = "scope-app-recursive"
description = "This scope permits recursive access to the complete `$APP` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$APP` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$APP"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APP/**" path = "$APP/**"
@ -16,6 +18,8 @@ path = "$APP/**"
identifier = "scope-app" identifier = "scope-app"
description = "This scope permits access to all files and list content of top level directories in the `$APP`folder." description = "This scope permits access to all files and list content of top level directories in the `$APP`folder."
[[permission.scope.allow]]
path = "$APP"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APP/*" path = "$APP/*"
@ -24,7 +28,7 @@ identifier = "scope-app-index"
description = "This scope permits to list all files and folders in the `$APP`folder." description = "This scope permits to list all files and folders in the `$APP`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APP/" path = "$APP"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-appcache-recursive" identifier = "scope-appcache-recursive"
description = "This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$APPCACHE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPCACHE/**" path = "$APPCACHE/**"
@ -16,6 +18,8 @@ path = "$APPCACHE/**"
identifier = "scope-appcache" identifier = "scope-appcache"
description = "This scope permits access to all files and list content of top level directories in the `$APPCACHE`folder." description = "This scope permits access to all files and list content of top level directories in the `$APPCACHE`folder."
[[permission.scope.allow]]
path = "$APPCACHE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPCACHE/*" path = "$APPCACHE/*"
@ -24,7 +28,7 @@ identifier = "scope-appcache-index"
description = "This scope permits to list all files and folders in the `$APPCACHE`folder." description = "This scope permits to list all files and folders in the `$APPCACHE`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPCACHE/" path = "$APPCACHE"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-appconfig-recursive" identifier = "scope-appconfig-recursive"
description = "This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$APPCONFIG"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPCONFIG/**" path = "$APPCONFIG/**"
@ -16,6 +18,8 @@ path = "$APPCONFIG/**"
identifier = "scope-appconfig" identifier = "scope-appconfig"
description = "This scope permits access to all files and list content of top level directories in the `$APPCONFIG`folder." description = "This scope permits access to all files and list content of top level directories in the `$APPCONFIG`folder."
[[permission.scope.allow]]
path = "$APPCONFIG"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPCONFIG/*" path = "$APPCONFIG/*"
@ -24,7 +28,7 @@ identifier = "scope-appconfig-index"
description = "This scope permits to list all files and folders in the `$APPCONFIG`folder." description = "This scope permits to list all files and folders in the `$APPCONFIG`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPCONFIG/" path = "$APPCONFIG"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-appdata-recursive" identifier = "scope-appdata-recursive"
description = "This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$APPDATA"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPDATA/**" path = "$APPDATA/**"
@ -16,6 +18,8 @@ path = "$APPDATA/**"
identifier = "scope-appdata" identifier = "scope-appdata"
description = "This scope permits access to all files and list content of top level directories in the `$APPDATA`folder." description = "This scope permits access to all files and list content of top level directories in the `$APPDATA`folder."
[[permission.scope.allow]]
path = "$APPDATA"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPDATA/*" path = "$APPDATA/*"
@ -24,7 +28,7 @@ identifier = "scope-appdata-index"
description = "This scope permits to list all files and folders in the `$APPDATA`folder." description = "This scope permits to list all files and folders in the `$APPDATA`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPDATA/" path = "$APPDATA"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-applocaldata-recursive" identifier = "scope-applocaldata-recursive"
description = "This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$APPLOCALDATA"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPLOCALDATA/**" path = "$APPLOCALDATA/**"
@ -16,6 +18,8 @@ path = "$APPLOCALDATA/**"
identifier = "scope-applocaldata" identifier = "scope-applocaldata"
description = "This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA`folder." description = "This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA`folder."
[[permission.scope.allow]]
path = "$APPLOCALDATA"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPLOCALDATA/*" path = "$APPLOCALDATA/*"
@ -24,7 +28,7 @@ identifier = "scope-applocaldata-index"
description = "This scope permits to list all files and folders in the `$APPLOCALDATA`folder." description = "This scope permits to list all files and folders in the `$APPLOCALDATA`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPLOCALDATA/" path = "$APPLOCALDATA"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-applog-recursive" identifier = "scope-applog-recursive"
description = "This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$APPLOG"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPLOG/**" path = "$APPLOG/**"
@ -16,6 +18,8 @@ path = "$APPLOG/**"
identifier = "scope-applog" identifier = "scope-applog"
description = "This scope permits access to all files and list content of top level directories in the `$APPLOG`folder." description = "This scope permits access to all files and list content of top level directories in the `$APPLOG`folder."
[[permission.scope.allow]]
path = "$APPLOG"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPLOG/*" path = "$APPLOG/*"
@ -24,7 +28,7 @@ identifier = "scope-applog-index"
description = "This scope permits to list all files and folders in the `$APPLOG`folder." description = "This scope permits to list all files and folders in the `$APPLOG`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$APPLOG/" path = "$APPLOG"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-audio-recursive" identifier = "scope-audio-recursive"
description = "This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$AUDIO"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$AUDIO/**" path = "$AUDIO/**"
@ -16,6 +18,8 @@ path = "$AUDIO/**"
identifier = "scope-audio" identifier = "scope-audio"
description = "This scope permits access to all files and list content of top level directories in the `$AUDIO`folder." description = "This scope permits access to all files and list content of top level directories in the `$AUDIO`folder."
[[permission.scope.allow]]
path = "$AUDIO"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$AUDIO/*" path = "$AUDIO/*"
@ -24,7 +28,7 @@ identifier = "scope-audio-index"
description = "This scope permits to list all files and folders in the `$AUDIO`folder." description = "This scope permits to list all files and folders in the `$AUDIO`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$AUDIO/" path = "$AUDIO"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-cache-recursive" identifier = "scope-cache-recursive"
description = "This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$CACHE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$CACHE/**" path = "$CACHE/**"
@ -16,6 +18,8 @@ path = "$CACHE/**"
identifier = "scope-cache" identifier = "scope-cache"
description = "This scope permits access to all files and list content of top level directories in the `$CACHE`folder." description = "This scope permits access to all files and list content of top level directories in the `$CACHE`folder."
[[permission.scope.allow]]
path = "$CACHE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$CACHE/*" path = "$CACHE/*"
@ -24,7 +28,7 @@ identifier = "scope-cache-index"
description = "This scope permits to list all files and folders in the `$CACHE`folder." description = "This scope permits to list all files and folders in the `$CACHE`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$CACHE/" path = "$CACHE"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-config-recursive" identifier = "scope-config-recursive"
description = "This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$CONFIG"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$CONFIG/**" path = "$CONFIG/**"
@ -16,6 +18,8 @@ path = "$CONFIG/**"
identifier = "scope-config" identifier = "scope-config"
description = "This scope permits access to all files and list content of top level directories in the `$CONFIG`folder." description = "This scope permits access to all files and list content of top level directories in the `$CONFIG`folder."
[[permission.scope.allow]]
path = "$CONFIG"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$CONFIG/*" path = "$CONFIG/*"
@ -24,7 +28,7 @@ identifier = "scope-config-index"
description = "This scope permits to list all files and folders in the `$CONFIG`folder." description = "This scope permits to list all files and folders in the `$CONFIG`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$CONFIG/" path = "$CONFIG"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-data-recursive" identifier = "scope-data-recursive"
description = "This scope permits recursive access to the complete `$DATA` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$DATA` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$DATA"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DATA/**" path = "$DATA/**"
@ -16,6 +18,8 @@ path = "$DATA/**"
identifier = "scope-data" identifier = "scope-data"
description = "This scope permits access to all files and list content of top level directories in the `$DATA`folder." description = "This scope permits access to all files and list content of top level directories in the `$DATA`folder."
[[permission.scope.allow]]
path = "$DATA"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DATA/*" path = "$DATA/*"
@ -24,7 +28,7 @@ identifier = "scope-data-index"
description = "This scope permits to list all files and folders in the `$DATA`folder." description = "This scope permits to list all files and folders in the `$DATA`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DATA/" path = "$DATA"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-desktop-recursive" identifier = "scope-desktop-recursive"
description = "This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$DESKTOP"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DESKTOP/**" path = "$DESKTOP/**"
@ -16,6 +18,8 @@ path = "$DESKTOP/**"
identifier = "scope-desktop" identifier = "scope-desktop"
description = "This scope permits access to all files and list content of top level directories in the `$DESKTOP`folder." description = "This scope permits access to all files and list content of top level directories in the `$DESKTOP`folder."
[[permission.scope.allow]]
path = "$DESKTOP"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DESKTOP/*" path = "$DESKTOP/*"
@ -24,7 +28,7 @@ identifier = "scope-desktop-index"
description = "This scope permits to list all files and folders in the `$DESKTOP`folder." description = "This scope permits to list all files and folders in the `$DESKTOP`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DESKTOP/" path = "$DESKTOP"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-document-recursive" identifier = "scope-document-recursive"
description = "This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$DOCUMENT"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DOCUMENT/**" path = "$DOCUMENT/**"
@ -16,6 +18,8 @@ path = "$DOCUMENT/**"
identifier = "scope-document" identifier = "scope-document"
description = "This scope permits access to all files and list content of top level directories in the `$DOCUMENT`folder." description = "This scope permits access to all files and list content of top level directories in the `$DOCUMENT`folder."
[[permission.scope.allow]]
path = "$DOCUMENT"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DOCUMENT/*" path = "$DOCUMENT/*"
@ -24,7 +28,7 @@ identifier = "scope-document-index"
description = "This scope permits to list all files and folders in the `$DOCUMENT`folder." description = "This scope permits to list all files and folders in the `$DOCUMENT`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DOCUMENT/" path = "$DOCUMENT"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-download-recursive" identifier = "scope-download-recursive"
description = "This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$DOWNLOAD"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DOWNLOAD/**" path = "$DOWNLOAD/**"
@ -16,6 +18,8 @@ path = "$DOWNLOAD/**"
identifier = "scope-download" identifier = "scope-download"
description = "This scope permits access to all files and list content of top level directories in the `$DOWNLOAD`folder." description = "This scope permits access to all files and list content of top level directories in the `$DOWNLOAD`folder."
[[permission.scope.allow]]
path = "$DOWNLOAD"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DOWNLOAD/*" path = "$DOWNLOAD/*"
@ -24,7 +28,7 @@ identifier = "scope-download-index"
description = "This scope permits to list all files and folders in the `$DOWNLOAD`folder." description = "This scope permits to list all files and folders in the `$DOWNLOAD`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$DOWNLOAD/" path = "$DOWNLOAD"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-exe-recursive" identifier = "scope-exe-recursive"
description = "This scope permits recursive access to the complete `$EXE` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$EXE` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$EXE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$EXE/**" path = "$EXE/**"
@ -16,6 +18,8 @@ path = "$EXE/**"
identifier = "scope-exe" identifier = "scope-exe"
description = "This scope permits access to all files and list content of top level directories in the `$EXE`folder." description = "This scope permits access to all files and list content of top level directories in the `$EXE`folder."
[[permission.scope.allow]]
path = "$EXE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$EXE/*" path = "$EXE/*"
@ -24,7 +28,7 @@ identifier = "scope-exe-index"
description = "This scope permits to list all files and folders in the `$EXE`folder." description = "This scope permits to list all files and folders in the `$EXE`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$EXE/" path = "$EXE"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-font-recursive" identifier = "scope-font-recursive"
description = "This scope permits recursive access to the complete `$FONT` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$FONT` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$FONT"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$FONT/**" path = "$FONT/**"
@ -16,6 +18,8 @@ path = "$FONT/**"
identifier = "scope-font" identifier = "scope-font"
description = "This scope permits access to all files and list content of top level directories in the `$FONT`folder." description = "This scope permits access to all files and list content of top level directories in the `$FONT`folder."
[[permission.scope.allow]]
path = "$FONT"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$FONT/*" path = "$FONT/*"
@ -24,7 +28,7 @@ identifier = "scope-font-index"
description = "This scope permits to list all files and folders in the `$FONT`folder." description = "This scope permits to list all files and folders in the `$FONT`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$FONT/" path = "$FONT"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-home-recursive" identifier = "scope-home-recursive"
description = "This scope permits recursive access to the complete `$HOME` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$HOME` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$HOME"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$HOME/**" path = "$HOME/**"
@ -16,6 +18,8 @@ path = "$HOME/**"
identifier = "scope-home" identifier = "scope-home"
description = "This scope permits access to all files and list content of top level directories in the `$HOME`folder." description = "This scope permits access to all files and list content of top level directories in the `$HOME`folder."
[[permission.scope.allow]]
path = "$HOME"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$HOME/*" path = "$HOME/*"
@ -24,7 +28,7 @@ identifier = "scope-home-index"
description = "This scope permits to list all files and folders in the `$HOME`folder." description = "This scope permits to list all files and folders in the `$HOME`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$HOME/" path = "$HOME"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-localdata-recursive" identifier = "scope-localdata-recursive"
description = "This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$LOCALDATA"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$LOCALDATA/**" path = "$LOCALDATA/**"
@ -16,6 +18,8 @@ path = "$LOCALDATA/**"
identifier = "scope-localdata" identifier = "scope-localdata"
description = "This scope permits access to all files and list content of top level directories in the `$LOCALDATA`folder." description = "This scope permits access to all files and list content of top level directories in the `$LOCALDATA`folder."
[[permission.scope.allow]]
path = "$LOCALDATA"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$LOCALDATA/*" path = "$LOCALDATA/*"
@ -24,7 +28,7 @@ identifier = "scope-localdata-index"
description = "This scope permits to list all files and folders in the `$LOCALDATA`folder." description = "This scope permits to list all files and folders in the `$LOCALDATA`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$LOCALDATA/" path = "$LOCALDATA"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-log-recursive" identifier = "scope-log-recursive"
description = "This scope permits recursive access to the complete `$LOG` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$LOG` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$LOG"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$LOG/**" path = "$LOG/**"
@ -16,6 +18,8 @@ path = "$LOG/**"
identifier = "scope-log" identifier = "scope-log"
description = "This scope permits access to all files and list content of top level directories in the `$LOG`folder." description = "This scope permits access to all files and list content of top level directories in the `$LOG`folder."
[[permission.scope.allow]]
path = "$LOG"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$LOG/*" path = "$LOG/*"
@ -24,7 +28,7 @@ identifier = "scope-log-index"
description = "This scope permits to list all files and folders in the `$LOG`folder." description = "This scope permits to list all files and folders in the `$LOG`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$LOG/" path = "$LOG"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-picture-recursive" identifier = "scope-picture-recursive"
description = "This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$PICTURE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$PICTURE/**" path = "$PICTURE/**"
@ -16,6 +18,8 @@ path = "$PICTURE/**"
identifier = "scope-picture" identifier = "scope-picture"
description = "This scope permits access to all files and list content of top level directories in the `$PICTURE`folder." description = "This scope permits access to all files and list content of top level directories in the `$PICTURE`folder."
[[permission.scope.allow]]
path = "$PICTURE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$PICTURE/*" path = "$PICTURE/*"
@ -24,7 +28,7 @@ identifier = "scope-picture-index"
description = "This scope permits to list all files and folders in the `$PICTURE`folder." description = "This scope permits to list all files and folders in the `$PICTURE`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$PICTURE/" path = "$PICTURE"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-public-recursive" identifier = "scope-public-recursive"
description = "This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$PUBLIC"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$PUBLIC/**" path = "$PUBLIC/**"
@ -16,6 +18,8 @@ path = "$PUBLIC/**"
identifier = "scope-public" identifier = "scope-public"
description = "This scope permits access to all files and list content of top level directories in the `$PUBLIC`folder." description = "This scope permits access to all files and list content of top level directories in the `$PUBLIC`folder."
[[permission.scope.allow]]
path = "$PUBLIC"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$PUBLIC/*" path = "$PUBLIC/*"
@ -24,7 +28,7 @@ identifier = "scope-public-index"
description = "This scope permits to list all files and folders in the `$PUBLIC`folder." description = "This scope permits to list all files and folders in the `$PUBLIC`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$PUBLIC/" path = "$PUBLIC"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-resource-recursive" identifier = "scope-resource-recursive"
description = "This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$RESOURCE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$RESOURCE/**" path = "$RESOURCE/**"
@ -16,6 +18,8 @@ path = "$RESOURCE/**"
identifier = "scope-resource" identifier = "scope-resource"
description = "This scope permits access to all files and list content of top level directories in the `$RESOURCE`folder." description = "This scope permits access to all files and list content of top level directories in the `$RESOURCE`folder."
[[permission.scope.allow]]
path = "$RESOURCE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$RESOURCE/*" path = "$RESOURCE/*"
@ -24,7 +28,7 @@ identifier = "scope-resource-index"
description = "This scope permits to list all files and folders in the `$RESOURCE`folder." description = "This scope permits to list all files and folders in the `$RESOURCE`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$RESOURCE/" path = "$RESOURCE"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-runtime-recursive" identifier = "scope-runtime-recursive"
description = "This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$RUNTIME"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$RUNTIME/**" path = "$RUNTIME/**"
@ -16,6 +18,8 @@ path = "$RUNTIME/**"
identifier = "scope-runtime" identifier = "scope-runtime"
description = "This scope permits access to all files and list content of top level directories in the `$RUNTIME`folder." description = "This scope permits access to all files and list content of top level directories in the `$RUNTIME`folder."
[[permission.scope.allow]]
path = "$RUNTIME"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$RUNTIME/*" path = "$RUNTIME/*"
@ -24,7 +28,7 @@ identifier = "scope-runtime-index"
description = "This scope permits to list all files and folders in the `$RUNTIME`folder." description = "This scope permits to list all files and folders in the `$RUNTIME`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$RUNTIME/" path = "$RUNTIME"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-temp-recursive" identifier = "scope-temp-recursive"
description = "This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$TEMP"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$TEMP/**" path = "$TEMP/**"
@ -16,6 +18,8 @@ path = "$TEMP/**"
identifier = "scope-temp" identifier = "scope-temp"
description = "This scope permits access to all files and list content of top level directories in the `$TEMP`folder." description = "This scope permits access to all files and list content of top level directories in the `$TEMP`folder."
[[permission.scope.allow]]
path = "$TEMP"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$TEMP/*" path = "$TEMP/*"
@ -24,7 +28,7 @@ identifier = "scope-temp-index"
description = "This scope permits to list all files and folders in the `$TEMP`folder." description = "This scope permits to list all files and folders in the `$TEMP`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$TEMP/" path = "$TEMP"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-template-recursive" identifier = "scope-template-recursive"
description = "This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$TEMPLATE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$TEMPLATE/**" path = "$TEMPLATE/**"
@ -16,6 +18,8 @@ path = "$TEMPLATE/**"
identifier = "scope-template" identifier = "scope-template"
description = "This scope permits access to all files and list content of top level directories in the `$TEMPLATE`folder." description = "This scope permits access to all files and list content of top level directories in the `$TEMPLATE`folder."
[[permission.scope.allow]]
path = "$TEMPLATE"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$TEMPLATE/*" path = "$TEMPLATE/*"
@ -24,7 +28,7 @@ identifier = "scope-template-index"
description = "This scope permits to list all files and folders in the `$TEMPLATE`folder." description = "This scope permits to list all files and folders in the `$TEMPLATE`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$TEMPLATE/" path = "$TEMPLATE"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,6 +9,8 @@
identifier = "scope-video-recursive" identifier = "scope-video-recursive"
description = "This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files." description = "This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files."
[[permission.scope.allow]]
path = "$VIDEO"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$VIDEO/**" path = "$VIDEO/**"
@ -16,6 +18,8 @@ path = "$VIDEO/**"
identifier = "scope-video" identifier = "scope-video"
description = "This scope permits access to all files and list content of top level directories in the `$VIDEO`folder." description = "This scope permits access to all files and list content of top level directories in the `$VIDEO`folder."
[[permission.scope.allow]]
path = "$VIDEO"
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$VIDEO/*" path = "$VIDEO/*"
@ -24,7 +28,7 @@ identifier = "scope-video-index"
description = "This scope permits to list all files and folders in the `$VIDEO`folder." description = "This scope permits to list all files and folders in the `$VIDEO`folder."
[[permission.scope.allow]] [[permission.scope.allow]]
path = "$VIDEO/" path = "$VIDEO"
# Sets Section # Sets Section
# This section combines the scope elements with enablement of commands # This section combines the scope elements with enablement of commands

@ -9,18 +9,60 @@ use tauri::{
ipc::{CommandScope, GlobalScope}, ipc::{CommandScope, GlobalScope},
path::{BaseDirectory, SafePathBuf}, path::{BaseDirectory, SafePathBuf},
utils::config::FsScope, utils::config::FsScope,
Manager, Resource, ResourceId, Runtime, Webview, AppHandle, Manager, Resource, ResourceId, Runtime, Webview,
}; };
use std::{ use std::{
fs::File, fs::File,
io::{BufReader, Lines, Read, Write}, io::{BufReader, Lines, Read, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr,
sync::Mutex, sync::Mutex,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use crate::{scope::Entry, Error, FsExt}; use crate::{scope::Entry, Error, FilePath, FsExt};
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum SafeFilePath {
Url(url::Url),
Path(SafePathBuf),
}
impl From<SafeFilePath> for FilePath {
fn from(value: SafeFilePath) -> Self {
match value {
SafeFilePath::Url(url) => FilePath::Url(url),
SafeFilePath::Path(p) => FilePath::Path(p.as_ref().to_owned()),
}
}
}
impl FromStr for SafeFilePath {
type Err = CommandError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(url) = url::Url::from_str(s) {
Ok(Self::Url(url))
} else {
Ok(Self::Path(SafePathBuf::new(s.into())?))
}
}
}
impl SafeFilePath {
#[inline]
fn into_path(self) -> CommandResult<SafePathBuf> {
match self {
Self::Url(url) => SafePathBuf::new(
url.to_file_path()
.map_err(|_| format!("failed to get path from {url}"))?,
)
.map_err(Into::into),
Self::Path(p) => Ok(p),
}
}
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum CommandError { pub enum CommandError {
@ -31,6 +73,8 @@ pub enum CommandError {
#[error(transparent)] #[error(transparent)]
Tauri(#[from] tauri::Error), Tauri(#[from] tauri::Error),
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
UrlParseError(#[from] url::ParseError), UrlParseError(#[from] url::ParseError),
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
#[error(transparent)] #[error(transparent)]
@ -64,7 +108,7 @@ impl Serialize for CommandError {
pub type CommandResult<T> = std::result::Result<T, CommandError>; pub type CommandResult<T> = std::result::Result<T, CommandError>;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BaseOptions { pub struct BaseOptions {
base_dir: Option<BaseDirectory>, base_dir: Option<BaseDirectory>,
@ -75,7 +119,7 @@ pub fn create<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<ResourceId> { ) -> CommandResult<ResourceId> {
let resolved_path = resolve_path( let resolved_path = resolve_path(
@ -95,29 +139,13 @@ pub fn create<R: Runtime>(
Ok(rid) Ok(rid)
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OpenOptions { pub struct OpenOptions {
#[serde(flatten)] #[serde(flatten)]
base: BaseOptions, base: BaseOptions,
#[serde(default = "default_true")] #[serde(flatten)]
read: bool, options: crate::OpenOptions,
#[serde(default)]
write: bool,
#[serde(default)]
append: bool,
#[serde(default)]
truncate: bool,
#[serde(default)]
create: bool,
#[serde(default)]
create_new: bool,
#[allow(unused)]
mode: Option<u32>,
}
fn default_true() -> bool {
true
} }
#[tauri::command] #[tauri::command]
@ -125,44 +153,35 @@ pub fn open<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<OpenOptions>, options: Option<OpenOptions>,
) -> CommandResult<ResourceId> { ) -> CommandResult<ResourceId> {
let resolved_path = resolve_path( let (file, _path) = resolve_file(
&webview, &webview,
&global_scope, &global_scope,
&command_scope, &command_scope,
path, path,
options.as_ref().and_then(|o| o.base.base_dir), if let Some(opts) = options {
)?; OpenOptions {
base: opts.base,
let mut opts = std::fs::OpenOptions::new(); options: opts.options,
// default to read-only
opts.read(true);
if let Some(options) = options {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
if let Some(mode) = options.mode {
opts.mode(mode);
} }
} } else {
OpenOptions {
opts.read(options.read) base: BaseOptions { base_dir: None },
.create(options.create) options: crate::OpenOptions {
.write(options.write) read: true,
.truncate(options.truncate) write: false,
.append(options.append) truncate: false,
.create_new(options.create_new); create: false,
} create_new: false,
append: false,
let file = opts.open(&resolved_path).map_err(|e| { mode: None,
format!( custom_flags: None,
"failed to open file at path: {} with error: {e}", },
resolved_path.display() }
) },
})?; )?;
let rid = webview.resources_table().add(StdFileResource::new(file)); let rid = webview.resources_table().add(StdFileResource::new(file));
@ -186,8 +205,8 @@ pub async fn copy_file<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
from_path: SafePathBuf, from_path: SafeFilePath,
to_path: SafePathBuf, to_path: SafeFilePath,
options: Option<CopyFileOptions>, options: Option<CopyFileOptions>,
) -> CommandResult<()> { ) -> CommandResult<()> {
let resolved_from_path = resolve_path( let resolved_from_path = resolve_path(
@ -228,7 +247,7 @@ pub fn mkdir<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<MkdirOptions>, options: Option<MkdirOptions>,
) -> CommandResult<()> { ) -> CommandResult<()> {
let resolved_path = resolve_path( let resolved_path = resolve_path(
@ -295,7 +314,7 @@ pub async fn read_dir<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<Vec<DirEntry>> { ) -> CommandResult<Vec<DirEntry>> {
let resolved_path = resolve_path( let resolved_path = resolve_path(
@ -334,25 +353,35 @@ pub async fn read_file<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<tauri::ipc::Response> { ) -> CommandResult<tauri::ipc::Response> {
let resolved_path = resolve_path( let (mut file, path) = resolve_file(
&webview, &webview,
&global_scope, &global_scope,
&command_scope, &command_scope,
path, path,
options.as_ref().and_then(|o| o.base_dir), OpenOptions {
base: BaseOptions {
base_dir: options.as_ref().and_then(|o| o.base_dir),
},
options: crate::OpenOptions {
read: true,
..Default::default()
},
},
)?; )?;
std::fs::read(&resolved_path)
.map(tauri::ipc::Response::new) let mut contents = Vec::new();
.map_err(|e| {
format!( file.read_to_end(&mut contents).map_err(|e| {
"failed to read file at path: {} with error: {e}", format!(
resolved_path.display() "failed to read file as text at path: {} with error: {e}",
) path.display()
}) )
.map_err(Into::into) })?;
Ok(tauri::ipc::Response::new(contents))
} }
#[tauri::command] #[tauri::command]
@ -360,24 +389,35 @@ pub async fn read_text_file<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<String> { ) -> CommandResult<String> {
let resolved_path = resolve_path( let (mut file, path) = resolve_file(
&webview, &webview,
&global_scope, &global_scope,
&command_scope, &command_scope,
path, path,
options.as_ref().and_then(|o| o.base_dir), OpenOptions {
base: BaseOptions {
base_dir: options.as_ref().and_then(|o| o.base_dir),
},
options: crate::OpenOptions {
read: true,
..Default::default()
},
},
)?; )?;
std::fs::read_to_string(&resolved_path)
.map_err(|e| { let mut contents = String::new();
format!(
"failed to read file as text at path: {} with error: {e}", file.read_to_string(&mut contents).map_err(|e| {
resolved_path.display() format!(
) "failed to read file as text at path: {} with error: {e}",
}) path.display()
.map_err(Into::into) )
})?;
Ok(contents)
} }
#[tauri::command] #[tauri::command]
@ -385,7 +425,7 @@ pub fn read_text_file_lines<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<ResourceId> { ) -> CommandResult<ResourceId> {
use std::io::BufRead; use std::io::BufRead;
@ -441,7 +481,7 @@ pub fn remove<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<RemoveOptions>, options: Option<RemoveOptions>,
) -> CommandResult<()> { ) -> CommandResult<()> {
let resolved_path = resolve_path( let resolved_path = resolve_path(
@ -509,8 +549,8 @@ pub fn rename<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
old_path: SafePathBuf, old_path: SafeFilePath,
new_path: SafePathBuf, new_path: SafeFilePath,
options: Option<RenameOptions>, options: Option<RenameOptions>,
) -> CommandResult<()> { ) -> CommandResult<()> {
let resolved_old_path = resolve_path( let resolved_old_path = resolve_path(
@ -566,27 +606,109 @@ pub async fn seek<R: Runtime>(
.map_err(Into::into) .map_err(Into::into)
} }
#[cfg(target_os = "android")]
fn get_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Metadata>>(
metadata_fn: F,
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<std::fs::Metadata> {
match path {
SafeFilePath::Url(url) => {
let (file, path) = resolve_file(
webview,
global_scope,
command_scope,
SafeFilePath::Url(url),
OpenOptions {
base: BaseOptions { base_dir: None },
options: crate::OpenOptions {
read: true,
..Default::default()
},
},
)?;
file.metadata().map_err(|e| {
format!(
"failed to get metadata of path: {} with error: {e}",
path.display()
)
.into()
})
}
SafeFilePath::Path(p) => get_fs_metadata(
metadata_fn,
webview,
global_scope,
command_scope,
SafeFilePath::Path(p),
options,
),
}
}
#[cfg(not(target_os = "android"))]
fn get_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Metadata>>(
metadata_fn: F,
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<std::fs::Metadata> {
get_fs_metadata(
metadata_fn,
webview,
global_scope,
command_scope,
path,
options,
)
}
fn get_fs_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Metadata>>(
metadata_fn: F,
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<std::fs::Metadata> {
let resolved_path = resolve_path(
webview,
global_scope,
command_scope,
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] #[tauri::command]
pub fn stat<R: Runtime>( pub fn stat<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<FileInfo> { ) -> CommandResult<FileInfo> {
let resolved_path = resolve_path( let metadata = get_metadata(
|p| std::fs::metadata(p),
&webview, &webview,
&global_scope, &global_scope,
&command_scope, &command_scope,
path, path,
options.as_ref().and_then(|o| o.base_dir), options,
)?; )?;
let metadata = std::fs::metadata(&resolved_path).map_err(|e| {
format!(
"failed to get metadata of path: {} with error: {e}",
resolved_path.display()
)
})?;
Ok(get_stat(metadata)) Ok(get_stat(metadata))
} }
@ -595,22 +717,17 @@ pub fn lstat<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<FileInfo> { ) -> CommandResult<FileInfo> {
let resolved_path = resolve_path( let metadata = get_metadata(
|p| std::fs::symlink_metadata(p),
&webview, &webview,
&global_scope, &global_scope,
&command_scope, &command_scope,
path, path,
options.as_ref().and_then(|o| o.base_dir), options,
)?; )?;
let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| {
format!(
"failed to get metadata of path: {} with error: {e}",
resolved_path.display()
)
})?;
Ok(get_stat(metadata)) Ok(get_stat(metadata))
} }
@ -627,7 +744,7 @@ pub async fn truncate<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
len: Option<u64>, len: Option<u64>,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<()> { ) -> CommandResult<()> {
@ -704,49 +821,51 @@ fn write_file_inner<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: &GlobalScope<Entry>, global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>, command_scope: &CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
data: &[u8], data: &[u8],
options: Option<WriteFileOptions>, options: Option<WriteFileOptions>,
) -> CommandResult<()> { ) -> CommandResult<()> {
let resolved_path = resolve_path( let (mut file, path) = resolve_file(
&webview, &webview,
global_scope, global_scope,
command_scope, command_scope,
path, path,
options.as_ref().and_then(|o| o.base.base_dir), if let Some(opts) = options {
)?; OpenOptions {
base: opts.base,
let mut opts = std::fs::OpenOptions::new(); options: crate::OpenOptions {
// defaults read: false,
opts.read(false).write(true).truncate(true).create(true); write: true,
create: opts.create,
if let Some(options) = options { truncate: !opts.append,
#[cfg(unix)] append: opts.append,
{ create_new: opts.create_new,
use std::os::unix::fs::OpenOptionsExt; mode: opts.mode,
if let Some(mode) = options.mode { custom_flags: None,
opts.mode(mode); },
} }
} } else {
OpenOptions {
opts.create(options.create) base: BaseOptions { base_dir: None },
.append(options.append) options: crate::OpenOptions {
.truncate(!options.append) read: false,
.create_new(options.create_new); write: true,
} truncate: true,
create: true,
let mut file = opts.open(&resolved_path).map_err(|e| { create_new: false,
format!( append: false,
"failed to open file at path: {} with error: {e}", mode: None,
resolved_path.display() custom_flags: None,
) },
})?; }
},
)?;
file.write_all(data) file.write_all(data)
.map_err(|e| { .map_err(|e| {
format!( format!(
"failed to write bytes to file at path: {} with error: {e}", "failed to write bytes to file at path: {} with error: {e}",
resolved_path.display() path.display()
) )
}) })
.map_err(Into::into) .map_err(Into::into)
@ -768,7 +887,7 @@ pub async fn write_file<R: Runtime>(
p.to_str() p.to_str()
.map_err(|e| anyhow::anyhow!("invalid path: {e}").into()) .map_err(|e| anyhow::anyhow!("invalid path: {e}").into())
}) })
.and_then(|p| SafePathBuf::new(p.into()).map_err(CommandError::from))?; .and_then(|p| SafeFilePath::from_str(p).map_err(CommandError::from))?;
let options = request let options = request
.headers() .headers()
.get("options") .get("options")
@ -782,12 +901,13 @@ pub async fn write_file<R: Runtime>(
#[tauri::command] #[tauri::command]
pub async fn write_text_file<R: Runtime>( pub async fn write_text_file<R: Runtime>(
webview: Webview<R>, #[allow(unused)] app: AppHandle<R>,
global_scope: GlobalScope<Entry>, #[allow(unused)] webview: Webview<R>,
command_scope: CommandScope<Entry>, #[allow(unused)] global_scope: GlobalScope<Entry>,
path: SafePathBuf, #[allow(unused)] command_scope: CommandScope<Entry>,
path: SafeFilePath,
data: String, data: String,
options: Option<WriteFileOptions>, #[allow(unused)] options: Option<WriteFileOptions>,
) -> CommandResult<()> { ) -> CommandResult<()> {
write_file_inner( write_file_inner(
webview, webview,
@ -804,7 +924,7 @@ pub fn exists<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
options: Option<BaseOptions>, options: Option<BaseOptions>,
) -> CommandResult<bool> { ) -> CommandResult<bool> {
let resolved_path = resolve_path( let resolved_path = resolve_path(
@ -817,24 +937,87 @@ pub fn exists<R: Runtime>(
Ok(resolved_path.exists()) Ok(resolved_path.exists())
} }
#[cfg(not(target_os = "android"))]
pub fn resolve_file<R: Runtime>(
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
resolve_file_in_fs(webview, global_scope, command_scope, path, open_options)
}
fn resolve_file_in_fs<R: Runtime>(
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
let path = resolve_path(
webview,
global_scope,
command_scope,
path,
open_options.base.base_dir,
)?;
let file = std::fs::OpenOptions::from(open_options.options)
.open(&path)
.map_err(|e| {
format!(
"failed to open file at path: {} with error: {e}",
path.display()
)
})?;
Ok((file, path))
}
#[cfg(target_os = "android")]
pub fn resolve_file<R: Runtime>(
webview: &Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
match path {
SafeFilePath::Url(url) => {
let path = url.as_str().into();
let file = webview
.fs()
.open(SafeFilePath::Url(url), open_options.options)?;
Ok((file, path))
}
SafeFilePath::Path(path) => resolve_file_in_fs(
webview,
global_scope,
command_scope,
SafeFilePath::Path(path),
open_options,
),
}
}
pub fn resolve_path<R: Runtime>( pub fn resolve_path<R: Runtime>(
app: &Webview<R>, webview: &Webview<R>,
global_scope: &GlobalScope<Entry>, global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>, command_scope: &CommandScope<Entry>,
path: SafePathBuf, path: SafeFilePath,
base_dir: Option<BaseDirectory>, base_dir: Option<BaseDirectory>,
) -> CommandResult<PathBuf> { ) -> CommandResult<PathBuf> {
let path = file_url_to_safe_pathbuf(path)?; let path = path.into_path()?;
let path = if let Some(base_dir) = base_dir { let path = if let Some(base_dir) = base_dir {
app.path().resolve(&path, base_dir)? webview.path().resolve(&path, base_dir)?
} else { } else {
path.as_ref().to_path_buf() path.as_ref().to_path_buf()
}; };
let scope = tauri::scope::fs::Scope::new( let scope = tauri::scope::fs::Scope::new(
app, webview,
&FsScope::Scope { &FsScope::Scope {
allow: app allow: webview
.fs_scope() .fs_scope()
.allowed .allowed
.lock() .lock()
@ -844,7 +1027,7 @@ pub fn resolve_path<R: Runtime>(
.chain(global_scope.allows().iter().map(|e| e.path.clone())) .chain(global_scope.allows().iter().map(|e| e.path.clone()))
.chain(command_scope.allows().iter().map(|e| e.path.clone())) .chain(command_scope.allows().iter().map(|e| e.path.clone()))
.collect(), .collect(),
deny: app deny: webview
.fs_scope() .fs_scope()
.denied .denied
.lock() .lock()
@ -854,7 +1037,7 @@ pub fn resolve_path<R: Runtime>(
.chain(global_scope.denies().iter().map(|e| e.path.clone())) .chain(global_scope.denies().iter().map(|e| e.path.clone()))
.chain(command_scope.denies().iter().map(|e| e.path.clone())) .chain(command_scope.denies().iter().map(|e| e.path.clone()))
.collect(), .collect(),
require_literal_leading_dot: app.fs_scope().require_literal_leading_dot, require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot,
}, },
)?; )?;
@ -865,18 +1048,6 @@ pub fn resolve_path<R: Runtime>(
} }
} }
#[inline]
fn file_url_to_safe_pathbuf(path: SafePathBuf) -> CommandResult<SafePathBuf> {
if path.as_ref().starts_with("file:") {
let url = url::Url::parse(&path.display().to_string())?
.to_file_path()
.map_err(|_| "failed to get path from `file:` url")?;
SafePathBuf::new(url).map_err(Into::into)
} else {
Ok(path)
}
}
struct StdFileResource(Mutex<File>); struct StdFileResource(Mutex<File>);
impl StdFileResource { impl StdFileResource {

@ -0,0 +1,35 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::PathBuf;
use tauri::{AppHandle, Runtime};
use crate::{FilePath, OpenOptions};
pub struct Fs<R: Runtime>(pub(crate) AppHandle<R>);
fn path_or_err<P: Into<FilePath>>(p: P) -> std::io::Result<PathBuf> {
match p.into() {
FilePath::Path(p) => Ok(p),
FilePath::Url(u) if u.scheme() == "file" => u
.to_file_path()
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL")),
FilePath::Url(_) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot use a URL to load files on desktop and iOS",
)),
}
}
impl<R: Runtime> Fs<R> {
pub fn open<P: Into<FilePath>>(
&self,
path: P,
opts: OpenOptions,
) -> std::io::Result<std::fs::File> {
let path = path_or_err(path)?;
std::fs::OpenOptions::from(opts).open(path)
}
}

@ -23,6 +23,9 @@ pub enum Error {
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
#[error(transparent)] #[error(transparent)]
Watch(#[from] notify::Error), Watch(#[from] notify::Error),
#[cfg(target_os = "android")]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
} }
impl Serialize for Error { impl Serialize for Error {

@ -11,6 +11,15 @@
html_favicon_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::{
convert::Infallible,
fmt,
io::Read,
path::{Path, PathBuf},
str::FromStr,
};
use serde::Deserialize;
use tauri::{ use tauri::{
ipc::ScopeObject, ipc::ScopeObject,
plugin::{Builder as PluginBuilder, TauriPlugin}, plugin::{Builder as PluginBuilder, TauriPlugin},
@ -20,16 +29,375 @@ use tauri::{
mod commands; mod commands;
mod config; mod config;
#[cfg(not(target_os = "android"))]
mod desktop;
mod error; mod error;
#[cfg(target_os = "android")]
mod mobile;
#[cfg(target_os = "android")]
mod models;
mod scope; mod scope;
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
mod watcher; 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 error::Error;
pub use scope::{Event as ScopeEvent, Scope}; pub use scope::{Event as ScopeEvent, Scope};
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
/// Represents either a filesystem path or a URI pointing to a file
/// such as `file://` URIs or Android `content://` URIs.
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum FilePath {
Url(url::Url),
Path(PathBuf),
}
impl FromStr for FilePath {
type Err = Infallible;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if let Ok(url) = url::Url::from_str(s) {
Ok(Self::Url(url))
} else {
Ok(Self::Path(PathBuf::from(s)))
}
}
}
impl From<PathBuf> for FilePath {
fn from(value: PathBuf) -> Self {
Self::Path(value)
}
}
impl From<&Path> for FilePath {
fn from(value: &Path) -> Self {
Self::Path(value.to_owned())
}
}
impl From<&PathBuf> for FilePath {
fn from(value: &PathBuf) -> Self {
Self::Path(value.to_owned())
}
}
impl From<url::Url> for FilePath {
fn from(value: url::Url) -> Self {
Self::Url(value)
}
}
impl fmt::Display for FilePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Url(u) => u.fmt(f),
Self::Path(p) => p.display().fmt(f),
}
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenOptions {
#[serde(default = "default_true")]
read: bool,
#[serde(default)]
write: bool,
#[serde(default)]
append: bool,
#[serde(default)]
truncate: bool,
#[serde(default)]
create: bool,
#[serde(default)]
create_new: bool,
#[serde(default)]
mode: Option<u32>,
#[serde(default)]
custom_flags: Option<i32>,
}
fn default_true() -> bool {
true
}
impl From<OpenOptions> for std::fs::OpenOptions {
fn from(open_options: OpenOptions) -> Self {
let mut opts = std::fs::OpenOptions::new();
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
if let Some(mode) = open_options.mode {
opts.mode(mode);
}
if let Some(flags) = open_options.custom_flags {
opts.custom_flags(flags);
}
}
opts.read(open_options.read)
.write(open_options.write)
.create(open_options.create)
.append(open_options.append)
.truncate(open_options.truncate)
.create_new(open_options.create_new);
opts
}
}
impl OpenOptions {
/// Creates a blank new set of options ready for configuration.
///
/// All options are initially set to `false`.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let mut options = OpenOptions::new();
/// let file = options.read(true).open("foo.txt");
/// ```
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Sets the option for read access.
///
/// This option, when true, will indicate that the file should be
/// `read`-able if opened.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().read(true).open("foo.txt");
/// ```
pub fn read(&mut self, read: bool) -> &mut Self {
self.read = read;
self
}
/// Sets the option for write access.
///
/// This option, when true, will indicate that the file should be
/// `write`-able if opened.
///
/// If the file already exists, any write calls on it will overwrite its
/// contents, without truncating it.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().write(true).open("foo.txt");
/// ```
pub fn write(&mut self, write: bool) -> &mut Self {
self.write = write;
self
}
/// Sets the option for the append mode.
///
/// This option, when true, means that writes will append to a file instead
/// of overwriting previous contents.
/// Note that setting `.write(true).append(true)` has the same effect as
/// setting only `.append(true)`.
///
/// Append mode guarantees that writes will be positioned at the current end of file,
/// even when there are other processes or threads appending to the same file. This is
/// unlike <code>[seek]\([SeekFrom]::[End]\(0))</code> followed by `write()`, which
/// has a race between seeking and writing during which another writer can write, with
/// our `write()` overwriting their data.
///
/// Keep in mind that this does not necessarily guarantee that data appended by
/// different processes or threads does not interleave. The amount of data accepted a
/// single `write()` call depends on the operating system and file system. A
/// successful `write()` is allowed to write only part of the given data, so even if
/// you're careful to provide the whole message in a single call to `write()`, there
/// is no guarantee that it will be written out in full. If you rely on the filesystem
/// accepting the message in a single write, make sure that all data that belongs
/// together is written in one operation. This can be done by concatenating strings
/// before passing them to [`write()`].
///
/// If a file is opened with both read and append access, beware that after
/// opening, and after every write, the position for reading may be set at the
/// end of the file. So, before writing, save the current position (using
/// <code>[Seek]::[stream_position]</code>), and restore it before the next read.
///
/// ## Note
///
/// This function doesn't create the file if it doesn't exist. Use the
/// [`OpenOptions::create`] method to do so.
///
/// [`write()`]: Write::write "io::Write::write"
/// [`flush()`]: Write::flush "io::Write::flush"
/// [stream_position]: Seek::stream_position "io::Seek::stream_position"
/// [seek]: Seek::seek "io::Seek::seek"
/// [Current]: SeekFrom::Current "io::SeekFrom::Current"
/// [End]: SeekFrom::End "io::SeekFrom::End"
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().append(true).open("foo.txt");
/// ```
pub fn append(&mut self, append: bool) -> &mut Self {
self.append = append;
self
}
/// Sets the option for truncating a previous file.
///
/// If a file is successfully opened with this option set it will truncate
/// the file to 0 length if it already exists.
///
/// The file must be opened with write access for truncate to work.
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().write(true).truncate(true).open("foo.txt");
/// ```
pub fn truncate(&mut self, truncate: bool) -> &mut Self {
self.truncate = truncate;
self
}
/// Sets the option to create a new file, or open it if it already exists.
///
/// In order for the file to be created, [`OpenOptions::write`] or
/// [`OpenOptions::append`] access must be used.
///
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().write(true).create(true).open("foo.txt");
/// ```
pub fn create(&mut self, create: bool) -> &mut Self {
self.create = create;
self
}
/// Sets the option to create a new file, failing if it already exists.
///
/// No file is allowed to exist at the target location, also no (dangling) symlink. In this
/// way, if the call succeeds, the file returned is guaranteed to be new.
/// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`]
/// or another error based on the situation. See [`OpenOptions::open`] for a
/// non-exhaustive list of likely errors.
///
/// This option is useful because it is atomic. Otherwise between checking
/// whether a file exists and creating a new one, the file may have been
/// created by another process (a TOCTOU race condition / attack).
///
/// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are
/// ignored.
///
/// The file must be opened with write or append access in order to create
/// a new file.
///
/// [`.create()`]: OpenOptions::create
/// [`.truncate()`]: OpenOptions::truncate
/// [`AlreadyExists`]: io::ErrorKind::AlreadyExists
///
/// # Examples
///
/// ```no_run
/// use tauri_plugin_fs::OpenOptions;
///
/// let file = OpenOptions::new().write(true)
/// .create_new(true)
/// .open("foo.txt");
/// ```
pub fn create_new(&mut self, create_new: bool) -> &mut Self {
self.create_new = create_new;
self
}
}
#[cfg(unix)]
impl std::os::unix::fs::OpenOptionsExt for OpenOptions {
fn custom_flags(&mut self, flags: i32) -> &mut Self {
self.custom_flags.replace(flags);
self
}
fn mode(&mut self, mode: u32) -> &mut Self {
self.mode.replace(mode);
self
}
}
impl OpenOptions {
#[cfg(target_os = "android")]
fn android_mode(&self) -> String {
let mut mode = String::new();
if self.read {
mode.push('r');
}
if self.write {
mode.push('w');
}
if self.truncate {
mode.push('t');
}
if self.append {
mode.push('a');
}
mode
}
}
impl<R: Runtime> Fs<R> {
pub fn read_to_string<P: Into<FilePath>>(&self, path: P) -> std::io::Result<String> {
let mut s = String::new();
self.open(
path,
OpenOptions {
read: true,
..Default::default()
},
)?
.read_to_string(&mut s)?;
Ok(s)
}
pub fn read<P: Into<FilePath>>(&self, path: P) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.open(
path,
OpenOptions {
read: true,
..Default::default()
},
)?
.read_to_end(&mut buf)?;
Ok(buf)
}
}
// implement ScopeObject here instead of in the scope module because it is also used on the build script // 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 // and we don't want to add tauri as a build dependency
impl ScopeObject for scope::Entry { impl ScopeObject for scope::Entry {
@ -55,6 +423,9 @@ impl ScopeObject for scope::Entry {
pub trait FsExt<R: Runtime> { pub trait FsExt<R: Runtime> {
fn fs_scope(&self) -> &Scope; fn fs_scope(&self) -> &Scope;
fn try_fs_scope(&self) -> Option<&Scope>; fn try_fs_scope(&self) -> Option<&Scope>;
/// Cross platform file system APIs that also support manipulating Android files.
fn fs(&self) -> &Fs<R>;
} }
impl<R: Runtime, T: Manager<R>> FsExt<R> for T { impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
@ -65,6 +436,10 @@ impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
fn try_fs_scope(&self) -> Option<&Scope> { fn try_fs_scope(&self) -> Option<&Scope> {
self.try_state::<Scope>().map(|s| s.inner()) self.try_state::<Scope>().map(|s| s.inner())
} }
fn fs(&self) -> &Fs<R> {
self.state::<Fs<R>>().inner()
}
} }
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> { pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
@ -104,6 +479,15 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
.config() .config()
.as_ref() .as_ref()
.and_then(|c| c.require_literal_leading_dot); .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); app.manage(scope);
Ok(()) Ok(())
}) })

@ -0,0 +1,96 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{
plugin::{PluginApi, PluginHandle},
AppHandle, Runtime,
};
use crate::{models::*, FilePath, OpenOptions};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "com.plugin.fs";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_fs);
// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Fs<R>> {
#[cfg(target_os = "android")]
let handle = api
.register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin")
.unwrap();
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_android - intent - send)?;
Ok(Fs(handle))
}
/// Access to the android-intent-send APIs.
pub struct Fs<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> Fs<R> {
pub fn open<P: Into<FilePath>>(
&self,
path: P,
opts: OpenOptions,
) -> std::io::Result<std::fs::File> {
match path.into() {
FilePath::Url(u) => self
.resolve_content_uri(u.to_string(), opts.android_mode())
.map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("failed to open file: {e}"),
)
}),
FilePath::Path(p) => {
// tauri::utils::platform::resources_dir() returns a PathBuf with the Android asset URI prefix
// we must resolve that file with the Android API
if p.strip_prefix(tauri::utils::platform::ANDROID_ASSET_PROTOCOL_URI_PREFIX)
.is_ok()
{
self.resolve_content_uri(p.to_string_lossy(), opts.android_mode())
.map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("failed to open file: {e}"),
)
})
} else {
std::fs::OpenOptions::from(opts).open(p)
}
}
}
}
#[cfg(target_os = "android")]
fn resolve_content_uri(
&self,
uri: impl Into<String>,
mode: impl Into<String>,
) -> crate::Result<std::fs::File> {
#[cfg(target_os = "android")]
{
let result = self.0.run_mobile_plugin::<GetFileDescriptorResponse>(
"getFileDescriptor",
GetFileDescriptorPayload {
uri: uri.into(),
mode: mode.into(),
},
)?;
if let Some(fd) = result.fd {
Ok(unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(fd)
})
} else {
todo!()
}
}
}
}

@ -0,0 +1,18 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetFileDescriptorPayload {
pub uri: String,
pub mode: String,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetFileDescriptorResponse {
pub fd: Option<i32>,
}

@ -7,7 +7,7 @@ use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileI
use serde::Deserialize; use serde::Deserialize;
use tauri::{ use tauri::{
ipc::{Channel, CommandScope, GlobalScope}, ipc::{Channel, CommandScope, GlobalScope},
path::{BaseDirectory, SafePathBuf}, path::BaseDirectory,
Manager, Resource, ResourceId, Runtime, Webview, Manager, Resource, ResourceId, Runtime, Webview,
}; };
@ -22,7 +22,7 @@ use std::{
}; };
use crate::{ use crate::{
commands::{resolve_path, CommandResult}, commands::{resolve_path, CommandResult, SafeFilePath},
scope::Entry, scope::Entry,
}; };
@ -83,7 +83,7 @@ pub struct WatchOptions {
#[tauri::command] #[tauri::command]
pub async fn watch<R: Runtime>( pub async fn watch<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
paths: Vec<SafePathBuf>, paths: Vec<SafeFilePath>,
options: WatchOptions, options: WatchOptions,
on_event: Channel<Event>, on_event: Channel<Event>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,

@ -1,5 +1,9 @@
# Changelog # Changelog
## \[2.0.0-rc.2]
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
## \[2.0.0-rc.1] ## \[2.0.0-rc.1]
- [`5d170a54`](https://github.com/tauri-apps/plugins-workspace/commit/5d170a5444982dcc14135f6f1fc3e5da359f0eb0) ([#1671](https://github.com/tauri-apps/plugins-workspace/pull/1671) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.3. - [`5d170a54`](https://github.com/tauri-apps/plugins-workspace/commit/5d170a5444982dcc14135f6f1fc3e5da359f0eb0) ([#1671](https://github.com/tauri-apps/plugins-workspace/pull/1671) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.3.

@ -1,7 +1,7 @@
[package] [package]
name = "tauri-plugin-geolocation" name = "tauri-plugin-geolocation"
description = "Get and track the device's current position" description = "Get and track the device's current position"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

@ -6,28 +6,29 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "tauri-plugin-geolocation", name: "tauri-plugin-geolocation",
platforms: [ platforms: [
.iOS(.v13), .macOS(.v10_13),
], .iOS(.v13),
products: [ ],
// Products define the executables and libraries a package produces, and make them visible to other packages. products: [
.library( // Products define the executables and libraries a package produces, and make them visible to other packages.
name: "tauri-plugin-geolocation", .library(
type: .static, name: "tauri-plugin-geolocation",
targets: ["tauri-plugin-geolocation"]), type: .static,
], targets: ["tauri-plugin-geolocation"])
dependencies: [ ],
.package(name: "Tauri", path: "../.tauri/tauri-api") dependencies: [
], .package(name: "Tauri", path: "../.tauri/tauri-api")
targets: [ ],
// Targets are the basic building blocks of a package. A target can define a module or a test suite. targets: [
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets are the basic building blocks of a package. A target can define a module or a test suite.
.target( // Targets can depend on other targets in this package, and on products in packages this package depends on.
name: "tauri-plugin-geolocation", .target(
dependencies: [ name: "tauri-plugin-geolocation",
.byName(name: "Tauri") dependencies: [
], .byName(name: "Tauri")
path: "Sources") ],
] path: "Sources")
]
) )

@ -24,4 +24,4 @@ log = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
global-hotkey = { version = "0.5", features = ["serde"] } global-hotkey = { version = "0.6", features = ["serde"] }

@ -5,9 +5,14 @@
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error { pub enum Error {
#[error("{0}")] #[error("{0}")]
GlobalHotkey(String), GlobalHotkey(String),
#[error(transparent)]
RecvError(#[from] std::sync::mpsc::RecvError),
#[error(transparent)]
Tauri(#[from] tauri::Error),
} }
impl Serialize for Error { impl Serialize for Error {

@ -20,11 +20,11 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use global_hotkey::GlobalHotKeyEvent;
pub use global_hotkey::{ pub use global_hotkey::{
hotkey::{Code, HotKey as Shortcut, Modifiers}, hotkey::{Code, HotKey as Shortcut, Modifiers},
GlobalHotKeyEvent as ShortcutEvent, HotKeyState as ShortcutState, GlobalHotKeyEvent as ShortcutEvent, HotKeyState as ShortcutState,
}; };
use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager};
use serde::Serialize; use serde::Serialize;
use tauri::{ use tauri::{
ipc::Channel, ipc::Channel,
@ -60,13 +60,33 @@ struct RegisteredShortcut<R: Runtime> {
handler: Option<Arc<HandlerFn<R>>>, handler: Option<Arc<HandlerFn<R>>>,
} }
struct GlobalHotKeyManager(global_hotkey::GlobalHotKeyManager);
/// SAFETY: we ensure it is run on main thread only
unsafe impl Send for GlobalHotKeyManager {}
/// SAFETY: we ensure it is run on main thread only
unsafe impl Sync for GlobalHotKeyManager {}
pub struct GlobalShortcut<R: Runtime> { pub struct GlobalShortcut<R: Runtime> {
#[allow(dead_code)] #[allow(dead_code)]
app: AppHandle<R>, app: AppHandle<R>,
manager: GlobalHotKeyManager, manager: Arc<GlobalHotKeyManager>,
shortcuts: Arc<Mutex<HashMap<HotKeyId, RegisteredShortcut<R>>>>, shortcuts: Arc<Mutex<HashMap<HotKeyId, RegisteredShortcut<R>>>>,
} }
macro_rules! run_main_thread {
($handle:expr, $manager:expr, |$m:ident| $ex:expr) => {{
let (tx, rx) = std::sync::mpsc::channel();
let manager = $manager.clone();
let task = move || {
let f = |$m: &GlobalHotKeyManager| $ex;
let _ = tx.send(f(&*manager));
};
$handle.run_on_main_thread(task)?;
rx.recv()?
}};
}
impl<R: Runtime> GlobalShortcut<R> { impl<R: Runtime> GlobalShortcut<R> {
fn register_internal<F: Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static>( fn register_internal<F: Fn(&AppHandle<R>, &Shortcut, ShortcutEvent) + Send + Sync + 'static>(
&self, &self,
@ -75,8 +95,7 @@ impl<R: Runtime> GlobalShortcut<R> {
) -> Result<()> { ) -> Result<()> {
let id = shortcut.id(); let id = shortcut.id();
let handler = handler.map(|h| Arc::new(Box::new(h) as HandlerFn<R>)); let handler = handler.map(|h| Arc::new(Box::new(h) as HandlerFn<R>));
run_main_thread!(self.app, self.manager, |m| m.0.register(shortcut))?;
self.manager.register(shortcut)?;
self.shortcuts self.shortcuts
.lock() .lock()
.unwrap() .unwrap()
@ -95,7 +114,7 @@ impl<R: Runtime> GlobalShortcut<R> {
let mut shortcuts = self.shortcuts.lock().unwrap(); let mut shortcuts = self.shortcuts.lock().unwrap();
for shortcut in hotkeys { for shortcut in hotkeys {
self.manager.register(shortcut)?; run_main_thread!(self.app, self.manager, |m| m.0.register(shortcut))?;
shortcuts.insert( shortcuts.insert(
shortcut.id(), shortcut.id(),
RegisteredShortcut { RegisteredShortcut {
@ -167,7 +186,7 @@ impl<R: Runtime> GlobalShortcut<R> {
S::Error: std::error::Error, S::Error: std::error::Error,
{ {
let shortcut = try_into_shortcut(shortcut)?; let shortcut = try_into_shortcut(shortcut)?;
self.manager.unregister(shortcut)?; run_main_thread!(self.app, self.manager, |m| m.0.unregister(shortcut))?;
self.shortcuts.lock().unwrap().remove(&shortcut.id()); self.shortcuts.lock().unwrap().remove(&shortcut.id());
Ok(()) Ok(())
} }
@ -180,15 +199,19 @@ impl<R: Runtime> GlobalShortcut<R> {
where where
T::Error: std::error::Error, T::Error: std::error::Error,
{ {
let mut s = Vec::new(); let mut mapped_shortcuts = Vec::new();
for shortcut in shortcuts { for shortcut in shortcuts {
s.push(try_into_shortcut(shortcut)?); mapped_shortcuts.push(try_into_shortcut(shortcut)?);
} }
self.manager.unregister_all(&s)?; {
let mapped_shortcuts = mapped_shortcuts.clone();
#[rustfmt::skip]
run_main_thread!(self.app, self.manager, |m| m.0.unregister_all(&mapped_shortcuts))?;
}
let mut shortcuts = self.shortcuts.lock().unwrap(); let mut shortcuts = self.shortcuts.lock().unwrap();
for s in s { for s in mapped_shortcuts {
shortcuts.remove(&s.id()); shortcuts.remove(&s.id());
} }
@ -200,9 +223,9 @@ impl<R: Runtime> GlobalShortcut<R> {
let mut shortcuts = self.shortcuts.lock().unwrap(); let mut shortcuts = self.shortcuts.lock().unwrap();
let hotkeys = std::mem::take(&mut *shortcuts); let hotkeys = std::mem::take(&mut *shortcuts);
let hotkeys = hotkeys.values().map(|s| s.shortcut).collect::<Vec<_>>(); let hotkeys = hotkeys.values().map(|s| s.shortcut).collect::<Vec<_>>();
self.manager #[rustfmt::skip]
.unregister_all(hotkeys.as_slice()) let res = run_main_thread!(self.app, self.manager, |m| m.0.unregister_all(hotkeys.as_slice()));
.map_err(Into::into) res.map_err(Into::into)
} }
/// Determines whether the given shortcut is registered by this application or not. /// Determines whether the given shortcut is registered by this application or not.
@ -375,7 +398,7 @@ impl<R: Runtime> Builder<R> {
is_registered, is_registered,
]) ])
.setup(move |app, _api| { .setup(move |app, _api| {
let manager = GlobalHotKeyManager::new()?; let manager = global_hotkey::GlobalHotKeyManager::new()?;
let mut store = HashMap::<HotKeyId, RegisteredShortcut<R>>::new(); let mut store = HashMap::<HotKeyId, RegisteredShortcut<R>>::new();
for shortcut in shortcuts { for shortcut in shortcuts {
manager.register(shortcut)?; manager.register(shortcut)?;
@ -405,7 +428,7 @@ impl<R: Runtime> Builder<R> {
app.manage(GlobalShortcut { app.manage(GlobalShortcut {
app: app.clone(), app: app.clone(),
manager, manager: Arc::new(GlobalHotKeyManager(manager)),
shortcuts, shortcuts,
}); });
Ok(()) Ok(())

@ -1,5 +1,9 @@
# Changelog # Changelog
## \[2.0.0-rc.2]
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
## \[2.0.0-rc.1] ## \[2.0.0-rc.1]
### changes ### changes

@ -1,7 +1,7 @@
[package] [package]
name = "tauri-plugin-haptics" name = "tauri-plugin-haptics"
description = "Haptic feedback and vibrations on Android and iOS" description = "Haptic feedback and vibrations on Android and iOS"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

@ -6,28 +6,29 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "tauri-plugin-haptics", name: "tauri-plugin-haptics",
platforms: [ platforms: [
.iOS(.v13), .macOS(.v10_13),
], .iOS(.v13),
products: [ ],
// Products define the executables and libraries a package produces, and make them visible to other packages. products: [
.library( // Products define the executables and libraries a package produces, and make them visible to other packages.
name: "tauri-plugin-haptics", .library(
type: .static, name: "tauri-plugin-haptics",
targets: ["tauri-plugin-haptics"]), type: .static,
], targets: ["tauri-plugin-haptics"])
dependencies: [ ],
.package(name: "Tauri", path: "../.tauri/tauri-api") dependencies: [
], .package(name: "Tauri", path: "../.tauri/tauri-api")
targets: [ ],
// Targets are the basic building blocks of a package. A target can define a module or a test suite. targets: [
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets are the basic building blocks of a package. A target can define a module or a test suite.
.target( // Targets can depend on other targets in this package, and on products in packages this package depends on.
name: "tauri-plugin-haptics", .target(
dependencies: [ name: "tauri-plugin-haptics",
.byName(name: "Tauri") dependencies: [
], .byName(name: "Tauri")
path: "Sources") ],
] path: "Sources")
]
) )

@ -1,5 +1,9 @@
# Changelog # Changelog
## \[2.0.0-rc.1]
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
## \[2.0.0-rc.0] ## \[2.0.0-rc.0]
- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. - [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC.

@ -1,6 +1,6 @@
[package] [package]
name = "tauri-plugin-log" name = "tauri-plugin-log"
version = "2.0.0-rc.0" version = "2.0.0-rc.1"
description = "Configurable logging for your Tauri app." description = "Configurable logging for your Tauri app."
authors = { workspace = true } authors = { workspace = true }
license = { workspace = true } license = { workspace = true }

@ -6,28 +6,29 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "tauri-plugin-log", name: "tauri-plugin-log",
platforms: [ platforms: [
.iOS(.v11), .macOS(.v10_13),
], .iOS(.v11),
products: [ ],
// Products define the executables and libraries a package produces, and make them visible to other packages. products: [
.library( // Products define the executables and libraries a package produces, and make them visible to other packages.
name: "tauri-plugin-log", .library(
type: .static, name: "tauri-plugin-log",
targets: ["tauri-plugin-log"]), type: .static,
], targets: ["tauri-plugin-log"])
dependencies: [ ],
.package(name: "Tauri", path: "../.tauri/tauri-api") dependencies: [
], .package(name: "Tauri", path: "../.tauri/tauri-api")
targets: [ ],
// Targets are the basic building blocks of a package. A target can define a module or a test suite. targets: [
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets are the basic building blocks of a package. A target can define a module or a test suite.
.target( // Targets can depend on other targets in this package, and on products in packages this package depends on.
name: "tauri-plugin-log", .target(
dependencies: [ name: "tauri-plugin-log",
.byName(name: "Tauri") dependencies: [
], .byName(name: "Tauri")
path: "Sources") ],
] path: "Sources")
]
) )

@ -1,5 +1,9 @@
# Changelog # Changelog
## \[2.0.0-rc.2]
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
## \[2.0.0-rc.1] ## \[2.0.0-rc.1]
### changes ### changes

@ -1,6 +1,6 @@
[package] [package]
name = "tauri-plugin-nfc" name = "tauri-plugin-nfc"
version = "2.0.0-rc.1" version = "2.0.0-rc.2"
description = "Read and write NFC tags on Android and iOS." description = "Read and write NFC tags on Android and iOS."
edition = { workspace = true } edition = { workspace = true }
authors = { workspace = true } authors = { workspace = true }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save