From 69a1fa099c3143b6e426492f1c9d9cfbe56d2209 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Wed, 20 Dec 2023 03:08:34 +0200 Subject: [PATCH] feat(fs): improved API (#751) * feat(fs): improved API * fmt * fix unix builds * again * clippy * clippy * fix import in docs examples * fmt, clippy * Update linux.rs * add API for watch * fix with `watcher` feature flag * use baseDir for all commands * do not export close function * fix build * organize and address review comments * fmt * generated files * rename FsFile to FileHandle, move APIs and docs * extend example * extend `Resource` * actually extend it --------- Co-authored-by: FabianLars Co-authored-by: Lucas Nogueira --- .changes/fs-improved-apis.md | 6 + Cargo.lock | 300 ++-- Cargo.toml | 1 + examples/api/src/App.svelte | 17 +- examples/api/src/views/Dialog.svelte | 4 +- examples/api/src/views/FileSystem.svelte | 170 ++- plugins/deep-link/Cargo.toml | 8 +- plugins/fs/Cargo.toml | 2 + plugins/fs/guest-js/index.ts | 1333 ++++++++++++----- plugins/fs/src/api-iife.js | 2 +- plugins/fs/src/commands.rs | 964 ++++++++---- plugins/fs/src/lib.rs | 29 +- plugins/fs/src/watcher.rs | 98 +- plugins/http/Cargo.toml | 2 +- .../src/platform_impl/windows.rs | 2 +- plugins/updater/Cargo.toml | 2 +- 16 files changed, 2065 insertions(+), 875 deletions(-) create mode 100644 .changes/fs-improved-apis.md diff --git a/.changes/fs-improved-apis.md b/.changes/fs-improved-apis.md new file mode 100644 index 00000000..5b41a8ce --- /dev/null +++ b/.changes/fs-improved-apis.md @@ -0,0 +1,6 @@ +--- +"fs": "patch" +"fs-js": "patch" +--- + +The `fs` plugin received a major overhaul to add new APIs and changed existing APIs to be closer to Node.js and Deno APIs. diff --git a/Cargo.lock b/Cargo.lock index a94cb076..fad3d6c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", ] @@ -105,7 +105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", "zerocopy", @@ -271,9 +271,9 @@ dependencies = [ [[package]] name = "arboard" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc" +checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08" dependencies = [ "clipboard-win", "core-graphics 0.22.3", @@ -333,7 +333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e" dependencies = [ "concurrent-queue", - "event-listener 3.0.1", + "event-listener 3.1.0", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -341,9 +341,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f658e2baef915ba0f26f1f7c42bfb8e12f532a01f449a090ded75ae7a07e9ba2" +checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" dependencies = [ "brotli", "flate2", @@ -355,11 +355,11 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.7.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9936333f3d84275cb4010514544ae7fe0847760f4a242e8ce603b358615cad" +checksum = "fc5ea910c42e5ab19012bab31f53cb4d63d54c3a27730f9a833a88efcf4bb52d" dependencies = [ - "async-lock 3.0.0", + "async-lock 3.1.1", "async-task", "concurrent-queue", "fastrand 2.0.1", @@ -405,14 +405,14 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ed9d5715c2d329bf1b4da8d60455b99b187f27ba726df2883799af9af60997" dependencies = [ - "async-lock 3.0.0", + "async-lock 3.1.1", "cfg-if", "concurrent-queue", "futures-io", "futures-lite 2.0.1", "parking", "polling 3.3.0", - "rustix 0.38.21", + "rustix 0.38.25", "slab", "tracing", "waker-fn", @@ -430,11 +430,11 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e900cdcd39bb94a14487d3f7ef92ca222162e6c7c3fe7cb3550ea75fb486ed" +checksum = "655b9c7fe787d3b25cc0f804a1a8401790f0c5bc395beb5a64dc77d8de079105" dependencies = [ - "event-listener 3.0.1", + "event-listener 3.1.0", "event-listener-strategy", "pin-project-lite", ] @@ -450,9 +450,9 @@ dependencies = [ "async-signal", "blocking", "cfg-if", - "event-listener 3.0.1", + "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.21", + "rustix 0.38.25", "windows-sys 0.48.0", ] @@ -479,7 +479,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.21", + "rustix 0.38.25", "signal-hook-registry", "slab", "windows-sys 0.48.0", @@ -540,6 +540,16 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic-write-file" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c232177ba50b16fe7a4588495bd474a62a9e45a8e4ca6fd7d0b7ac29d164631e" +dependencies = [ + "nix 0.26.4", + "rand 0.8.5", +] + [[package]] name = "authenticator" version = "0.3.1" @@ -683,12 +693,12 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b30e660d766b7e9b47347d9b6558a17f1cfa22274034fa6f55b274b3e4620" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel", - "async-lock 3.0.0", + "async-lock 3.1.1", "async-task", "fastrand 2.0.1", "futures-io", @@ -813,12 +823,12 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.17.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1ece59890e746567b467253aea0adbe8a21784d0b025d8a306f66c391c2957" +checksum = "6ca592ad99e6a0fd4b95153406138b997cc26ccd3cd0aecdfd4fbdbf1519bd77" dependencies = [ "serde", - "toml 0.8.6", + "toml 0.8.8", ] [[package]] @@ -938,18 +948,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.7" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.7" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" dependencies = [ "anstream", "anstyle", @@ -1558,7 +1568,7 @@ checksum = "f54cc3e827ee1c3812239a9a41dede7b4d7d5d5464faa32d71bd7cba28ce2cb2" dependencies = [ "cc", "rustc_version", - "toml 0.8.6", + "toml 0.8.8", "vswhom", "winreg 0.51.0", ] @@ -1613,9 +1623,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ "log", "regex", @@ -1629,9 +1639,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ "libc", "windows-sys 0.48.0", @@ -1666,9 +1676,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" dependencies = [ "concurrent-queue", "parking", @@ -1681,7 +1691,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160" dependencies = [ - "event-listener 3.0.1", + "event-listener 3.1.0", "pin-project-lite", ] @@ -1924,7 +1934,11 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb" dependencies = [ + "fastrand 2.0.1", "futures-core", + "futures-io", + "memchr", + "parking", "pin-project-lite", ] @@ -2102,9 +2116,9 @@ dependencies = [ [[package]] name = "gethostname" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" dependencies = [ "libc", "winapi", @@ -2133,9 +2147,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "js-sys", @@ -2405,9 +2419,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes 1.5.0", "fnv", @@ -2415,7 +2429,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -2553,9 +2567,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes 1.5.0", "fnv", @@ -2823,7 +2837,7 @@ dependencies = [ "digest 0.10.7", "ed25519-zebra", "generic-array", - "getrandom 0.2.10", + "getrandom 0.2.11", "hmac", "pbkdf2", "serde", @@ -2903,7 +2917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.21", + "rustix 0.38.25", "windows-sys 0.48.0", ] @@ -3154,9 +3168,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -3206,9 +3220,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" @@ -3520,6 +3534,7 @@ dependencies = [ "cfg-if", "libc", "memoffset 0.7.1", + "pin-utils", ] [[package]] @@ -3572,9 +3587,9 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.9.0" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7b75c8958cb2eab3451538b32db8a7b74006abc33eb2e6a9a56d21e4775c2b" +checksum = "827c5edfa80235ded4ab3fe8e9dc619b4f866ef16fe9b1c6b8a7f8692c0f2226" dependencies = [ "log", "mac-notification-sys", @@ -4194,7 +4209,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", - "rustix 0.38.21", + "rustix 0.38.25", "tracing", "windows-sys 0.48.0", ] @@ -4369,9 +4384,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c78e758510582acc40acb90458401172d41f1016f8c9dde89e49677afb7eec1" +checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" dependencies = [ "bytes 1.5.0", "rand 0.8.5", @@ -4466,7 +4481,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -4528,7 +4543,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", "libredox", "thiserror", ] @@ -4629,7 +4644,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.25.2", + "webpki-roots", "winreg 0.50.0", ] @@ -4688,7 +4703,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" dependencies = [ "cc", - "getrandom 0.2.10", + "getrandom 0.2.11", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -4697,9 +4712,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +checksum = "6a3211b01eea83d80687da9eef70e39d65144a3894866a5153a2723e425a157f" dependencies = [ "const-oid", "digest 0.10.7", @@ -4770,22 +4785,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys 0.4.10", + "linux-raw-sys 0.4.11", "windows-sys 0.48.0", ] [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", "ring 0.17.5", @@ -4807,9 +4822,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64 0.21.5", ] @@ -4948,18 +4963,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.190" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", @@ -5135,9 +5150,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -5183,9 +5198,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" @@ -5271,9 +5286,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" dependencies = [ "sqlx-core", "sqlx-macros", @@ -5284,9 +5299,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ "ahash 0.8.6", "atoi", @@ -5323,14 +5338,14 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots 0.24.0", + "webpki-roots", ] [[package]] name = "sqlx-macros" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" dependencies = [ "proc-macro2", "quote", @@ -5341,10 +5356,11 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" dependencies = [ + "atomic-write-file", "dotenvy", "either", "heck", @@ -5367,9 +5383,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", "base64 0.21.5", @@ -5410,9 +5426,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", "base64 0.21.5", @@ -5450,9 +5466,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" dependencies = [ "atoi", "flume", @@ -5469,6 +5485,7 @@ dependencies = [ "time 0.3.30", "tracing", "url", + "urlencoding", ] [[package]] @@ -5677,7 +5694,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 0.8.6", + "toml 0.8.8", "version-compare", ] @@ -5762,7 +5779,7 @@ dependencies = [ "dirs-next", "embed_plist", "futures-util", - "getrandom 0.2.10", + "getrandom 0.2.11", "glob", "gtk", "heck", @@ -5984,8 +6001,10 @@ dependencies = [ "notify", "notify-debouncer-mini", "serde", + "serde_repr", "tauri", "thiserror", + "url", "uuid", ] @@ -6323,7 +6342,7 @@ dependencies = [ "brotli", "ctor", "dunce", - "getrandom 0.2.10", + "getrandom 0.2.11", "glob", "heck", "html5ever", @@ -6374,7 +6393,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall 0.4.1", - "rustix 0.38.21", + "rustix 0.38.25", "windows-sys 0.48.0", ] @@ -6520,9 +6539,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes 1.5.0", @@ -6591,7 +6610,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls", "tungstenite", - "webpki-roots 0.25.2", + "webpki-roots", ] [[package]] @@ -6622,14 +6641,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.7", + "toml_edit 0.21.0", ] [[package]] @@ -6659,6 +6678,17 @@ name = "toml_edit" version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ "indexmap 2.1.0", "serde", @@ -6708,9 +6738,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", @@ -6719,9 +6749,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -6956,6 +6986,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -6964,9 +7000,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8-width" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" [[package]] name = "utf8parse" @@ -6976,11 +7012,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -7218,15 +7254,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "webpki-roots" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki", -] - [[package]] name = "webpki-roots" version = "0.25.2" @@ -7365,14 +7392,15 @@ dependencies = [ [[package]] name = "window-vibrancy" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5931735e675b972fada30c7a402915d4d827aa5ef6c929c133d640c4b785e963" +checksum = "af6abc2b9c56bd95887825a1ce56cde49a2a97c07e28db465d541f5098a2656c" dependencies = [ "cocoa 0.25.0", "objc", "raw-window-handle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", + "windows-version", ] [[package]] @@ -7815,12 +7843,12 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" dependencies = [ - "gethostname 0.2.3", - "nix 0.24.3", + "gethostname 0.3.0", + "nix 0.26.4", "winapi", "winapi-wsapoll", "x11rb-protocol", @@ -7828,11 +7856,11 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" dependencies = [ - "nix 0.24.3", + "nix 0.26.4", ] [[package]] @@ -7933,18 +7961,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.25" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.25" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", @@ -7953,9 +7981,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index f63e4890..46738a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ tauri = "2.0.0-alpha.20" tauri-build = "2.0.0-alpha.13" serde_json = "1" thiserror = "1" +url = "2" [workspace.package] edition = "2021" diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte index 7e9ef010..151b692b 100644 --- a/examples/api/src/App.svelte +++ b/examples/api/src/App.svelte @@ -19,7 +19,7 @@ import Scanner from "./views/Scanner.svelte"; import Biometric from "./views/Biometric.svelte"; - import { onMount } from "svelte"; + import { onMount, tick } from "svelte"; import { ask } from "@tauri-apps/plugin-dialog"; import Nfc from "./views/Nfc.svelte"; @@ -178,30 +178,35 @@ // Console let messages = writable([]); - function onMessage(value) { + let consoleTextEl; + async function onMessage(value) { messages.update((r) => [ + ...r, { html: `
[${new Date().toLocaleTimeString()}]: ` +
           (typeof value === "string" ? value : JSON.stringify(value, null, 1)) +
           "
", }, - ...r, ]); + await tick(); + if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight; } // this function is renders HTML without sanitizing it so it's insecure // we only use it with our own input data - function insecureRenderHtml(html) { + async function insecureRenderHtml(html) { messages.update((r) => [ + ...r, { html: `
[${new Date().toLocaleTimeString()}]: ` +
           html +
           "
", }, - ...r, ]); + await tick(); + if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight; } function clear() { @@ -486,7 +491,7 @@
-
+
{#each $messages as r} {@html r.html} {/each} diff --git a/examples/api/src/views/Dialog.svelte b/examples/api/src/views/Dialog.svelte index 46f91a56..40df5403 100644 --- a/examples/api/src/views/Dialog.svelte +++ b/examples/api/src/views/Dialog.svelte @@ -1,6 +1,6 @@ -
+

- + + + + +
+ + +
- + {#if file} +
+ + +
+ {/if} +

diff --git a/plugins/deep-link/Cargo.toml b/plugins/deep-link/Cargo.toml index cc4095e6..c940451e 100644 --- a/plugins/deep-link/Cargo.toml +++ b/plugins/deep-link/Cargo.toml @@ -9,9 +9,9 @@ rust-version = { workspace = true } links = "tauri-plugin-deep-link" [package.metadata.docs.rs] -rustc-args = [ "--cfg", "docsrs" ] -rustdoc-args = [ "--cfg", "docsrs" ] -targets = [ "x86_64-linux-android" ] +rustc-args = ["--cfg", "docsrs"] +rustdoc-args = ["--cfg", "docsrs"] +targets = ["x86_64-linux-android"] [build-dependencies] serde = { workspace = true } @@ -24,4 +24,4 @@ serde_json = { workspace = true } tauri = { workspace = true } log = { workspace = true } thiserror = { workspace = true } -url = "2" +url = { workspace = true } diff --git a/plugins/fs/Cargo.toml b/plugins/fs/Cargo.toml index 8de63de1..0aaa061b 100644 --- a/plugins/fs/Cargo.toml +++ b/plugins/fs/Cargo.toml @@ -13,8 +13,10 @@ rustdoc-args = [ "--cfg", "docsrs" ] [dependencies] serde = { workspace = true } +serde_repr = "0.1" tauri = { workspace = true } thiserror = { workspace = true } +url = { workspace = true } anyhow = "1" uuid = { version = "1", features = [ "v4" ] } glob = "0.3" diff --git a/plugins/fs/guest-js/index.ts b/plugins/fs/guest-js/index.ts index a949c95a..545e800c 100644 --- a/plugins/fs/guest-js/index.ts +++ b/plugins/fs/guest-js/index.ts @@ -16,7 +16,7 @@ * * The scope configuration is an array of glob patterns describing folder paths that are allowed. * For instance, this scope configuration only allows accessing files on the - * *databases* folder of the {@link path.appDataDir | $APPDATA directory}: + * *databases* folder of the {@link https://beta.tauri.app/2/reference/js/core/namespacepath/#appdatadir | `$APPDATA` directory}: * ```json * { * "plugins": { @@ -27,16 +27,32 @@ * } * ``` * - * Notice the use of the `$APPDATA` variable. The value is injected at runtime, resolving to the {@link path.appDataDir | app data directory}. + * Notice the use of the `$APPDATA` variable. The value is injected at runtime, resolving to the {@link https://beta.tauri.app/2/reference/js/core/namespacepath/#appdatadir | app data directory}. + * * The available variables are: - * {@link path.appConfigDir | `$APPCONFIG`}, {@link path.appDataDir | `$APPDATA`}, {@link path.appLocalDataDir | `$APPLOCALDATA`}, - * {@link path.appCacheDir | `$APPCACHE`}, {@link path.appLogDir | `$APPLOG`}, - * {@link path.audioDir | `$AUDIO`}, {@link path.cacheDir | `$CACHE`}, {@link path.configDir | `$CONFIG`}, {@link path.dataDir | `$DATA`}, - * {@link path.localDataDir | `$LOCALDATA`}, {@link path.desktopDir | `$DESKTOP`}, {@link path.documentDir | `$DOCUMENT`}, - * {@link path.downloadDir | `$DOWNLOAD`}, {@link path.executableDir | `$EXE`}, {@link path.fontDir | `$FONT`}, {@link path.homeDir | `$HOME`}, - * {@link path.pictureDir | `$PICTURE`}, {@link path.publicDir | `$PUBLIC`}, {@link path.runtimeDir | `$RUNTIME`}, - * {@link path.templateDir | `$TEMPLATE`}, {@link path.videoDir | `$VIDEO`}, {@link path.resourceDir | `$RESOURCE`}, - * {@link os.tempdir | `$TEMP`}. + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#appconfigdir | $APPCONFIG}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#appdatadir | $APPDATA}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#appLocaldatadir | $APPLOCALDATA}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#appcachedir | $APPCACHE}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#applogdir | $APPLOG}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#audiodir | $AUDIO}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#cachedir | $CACHE}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#configdir | $CONFIG}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#datadir | $DATA}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#localdatadir | $LOCALDATA}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#desktopdir | $DESKTOP}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#documentdir | $DOCUMENT}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#downloaddir | $DOWNLOAD}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#executabledir | $EXE}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#fontdir | $FONT}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#homedir | $HOME}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#picturedir | $PICTURE}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#publicdir | $PUBLIC}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#runtimedir | $RUNTIME}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#templatedir | $TEMPLATE}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#videodir | $VIDEO}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#resourcedir | $RESOURCE}, + * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#tempdir | $TEMP}. * * Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access. * @@ -46,529 +62,994 @@ */ import { BaseDirectory } from "@tauri-apps/api/path"; +import { Channel, invoke, Resource } from "@tauri-apps/api/core"; -import { invoke } from "@tauri-apps/api/core"; - -interface Permissions { - /** - * `true` if these permissions describe a readonly (unwritable) file. - */ - readonly: boolean; - /** - * The underlying raw `st_mode` bits that contain the standard Unix permissions for this file. - */ - mode: number | undefined; +enum SeekMode { + Start = 0, + Current = 1, + End = 2, } /** - * Metadata information about a file. - * This structure is returned from the `metadata` function or method - * and represents known metadata about a file such as its permissions, size, modification times, etc. + * A FileInfo describes a file and is returned by `stat`, `lstat` or `fstat`. + * + * @since 2.0.0 */ -interface Metadata { +interface FileInfo { /** - * The last access time of this metadata. + * True if this is info for a regular file. Mutually exclusive to + * `FileInfo.isDirectory` and `FileInfo.isSymlink`. */ - accessedAt: Date; + isFile: boolean; /** - * The creation time listed in this metadata. + * True if this is info for a regular directory. Mutually exclusive to + * `FileInfo.isFile` and `FileInfo.isSymlink`. */ - createdAt: Date; + isDirectory: boolean; /** - * The last modification time listed in this metadata. + * True if this is info for a symlink. Mutually exclusive to + * `FileInfo.isFile` and `FileInfo.isDirectory`. */ - modifiedAt: Date; + isSymlink: boolean; /** - * `true` if this metadata is for a directory. + * The size of the file, in bytes. */ - isDir: boolean; + size: number; /** - * `true` if this metadata is for a regular file. + * The last modification time of the file. This corresponds to the `mtime` + * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This + * may not be available on all platforms. */ - isFile: boolean; + mtime: Date | null; /** - * `true` if this metadata is for a symbolic link. + * The last access time of the file. This corresponds to the `atime` + * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not + * be available on all platforms. */ - isSymlink: boolean; + atime: Date | null; /** - * The size of the file, in bytes, this metadata is for. + * The creation time of the file. This corresponds to the `birthtime` + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. */ - size: number; + birthtime: Date | null; + /** Whether this is a readonly (unwritable) file. */ + readonly: boolean; /** - * The permissions of the file this metadata is for. + * This field contains the file system attribute information for a file + * or directory. For possible values and their descriptions, see + * {@link https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants | File Attribute Constants} in the Windows Dev Center + * + * #### Platform-specific + * + * - **macOS / Linux / Android / iOS:** Unsupported. */ - permissions: Permissions; + fileAttributes: number | null; /** - * The ID of the device containing the file. Only available on Unix. + * ID of the device containing the file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - dev: number | undefined; + dev: number | null; /** - * The inode number. Only available on Unix. + * Inode number. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - ino: number | undefined; + ino: number | null; /** - * The rights applied to this file. Only available on Unix. + * The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - mode: number | undefined; + mode: number | null; /** - * The number of hard links pointing to this file. Only available on Unix. + * Number of hard links pointing to this file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - nlink: number | undefined; + nlink: number | null; /** - * The user ID of the owner of this file. Only available on Unix. + * User ID of the owner of this file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - uid: number | undefined; + uid: number | null; /** - * The group ID of the owner of this file. Only available on Unix. + * Group ID of the owner of this file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - gid: number | undefined; + gid: number | null; /** - * The device ID of this file (if it is a special one). Only available on Unix. + * Device ID of this file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - rdev: number | undefined; + rdev: number | null; /** - * The block size for filesystem I/O. Only available on Unix. + * Blocksize for filesystem I/O. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - blksize: number | undefined; + blksize: number | null; /** - * The number of blocks allocated to the file, in 512-byte units. Only available on Unix. + * Number of blocks allocated to the file, in 512-byte units. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. */ - blocks: number | undefined; + blocks: number | null; } -interface BackendMetadata { - accessedAtMs: number; - createdAtMs: number; - modifiedAtMs: number; - isDir: boolean; +interface UnparsedFileInfo { isFile: boolean; + isDirectory: boolean; isSymlink: boolean; size: number; - permissions: Permissions; - dev: number | undefined; - ino: number | undefined; - mode: number | undefined; - nlink: number | undefined; - uid: number | undefined; - gid: number | undefined; - rdev: number | undefined; - blksize: number | undefined; - blocks: number | undefined; + mtime: number | null; + atime: number | null; + birthtime: number | null; + readonly: boolean; + fileAttributes: number; + dev: number | null; + ino: number | null; + mode: number | null; + nlink: number | null; + uid: number | null; + gid: number | null; + rdev: number | null; + blksize: number | null; + blocks: number | null; +} +function parseFileInfo(r: UnparsedFileInfo): FileInfo { + return { + isFile: r.isFile, + isDirectory: r.isDirectory, + isSymlink: r.isSymlink, + size: r.size, + mtime: r.mtime != null ? new Date(r.mtime) : null, + atime: r.atime != null ? new Date(r.atime) : null, + birthtime: r.birthtime != null ? new Date(r.birthtime) : null, + readonly: r.readonly, + fileAttributes: r.fileAttributes, + dev: r.dev, + ino: r.ino, + mode: r.mode, + nlink: r.nlink, + uid: r.uid, + gid: r.gid, + rdev: r.rdev, + blksize: r.blksize, + blocks: r.blocks, + }; } /** + * The Tauri abstraction for reading and writing files. + * * @since 2.0.0 */ -interface FsOptions { - dir?: BaseDirectory; - // note that adding fields here needs a change in the writeBinaryFile check +class FileHandle extends Resource { + constructor(rid: number) { + super(rid); + } + + /** + * Reads up to `p.byteLength` bytes into `p`. It resolves to the number of + * bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error + * encountered. Even if `read()` resolves to `n` < `p.byteLength`, it may + * use all of `p` as scratch space during the call. If some data is + * available but not `p.byteLength` bytes, `read()` conventionally resolves + * to what is available instead of waiting for more. + * + * When `read()` encounters end-of-file condition, it resolves to EOF + * (`null`). + * + * When `read()` encounters an error, it rejects with an error. + * + * Callers should always process the `n` > `0` bytes returned before + * considering the EOF (`null`). Doing so correctly handles I/O errors that + * happen after reading some bytes and also both of the allowed EOF + * behaviors. + * + * @example + * ```typescript + * import { open, read, close, BaseDirectory } from "@tauri-apps/plugin-fs" + * // if "$APP/foo/bar.txt" contains the text "hello world": + * const file = await open("foo/bar.txt", { dir: BaseDirectory.App }); + * const buf = new Uint8Array(100); + * const numberOfBytesRead = await file.read(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * await close(file.rid); + * ``` + * + * @since 2.0.0 + */ + async read(buffer: Uint8Array): Promise { + if (buffer.byteLength === 0) { + return 0; + } + + const [data, nread] = await invoke<[number[], number]>("plugin:fs|read", { + rid: this.rid, + len: buffer.byteLength, + }); + + buffer.set(data); + + return nread === 0 ? null : nread; + } + + /** + * Seek sets the offset for the next `read()` or `write()` to offset, + * interpreted according to `whence`: `Start` means relative to the + * start of the file, `Current` means relative to the current offset, + * and `End` means relative to the end. Seek resolves to the new offset + * relative to the start of the file. + * + * Seeking to an offset before the start of the file is an error. Seeking to + * any positive offset is legal, but the behavior of subsequent I/O + * operations on the underlying object is implementation-dependent. + * It returns the number of cursor position. + * + * @example + * ```typescript + * import { open, seek, write, SeekMode, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * // Given hello.txt pointing to file with "Hello world", which is 11 bytes long: + * const file = await open('hello.txt', { read: true, write: true, truncate: true, create: true, dir: BaseDirectory.App }); + * await file.write(new TextEncoder().encode("Hello world"), { dir: BaseDirectory.App }); + * + * // Seek 6 bytes from the start of the file + * console.log(await file.seek(6, SeekMode.Start)); // "6" + * // Seek 2 more bytes from the current position + * console.log(await file.seek(2, SeekMode.Current)); // "8" + * // Seek backwards 2 bytes from the end of the file + * console.log(await file.seek(-2, SeekMode.End)); // "9" (e.g. 11-2) + * ``` + * + * @since 2.0.0 + */ + async seek(offset: number, whence: SeekMode): Promise { + return invoke("plugin:fs|seek", { + rid: this.rid, + offset, + whence, + }); + } + + /** + * Returns a {@linkcode FileInfo } for this file. + * + * @example + * ```typescript + * import { open, fstat, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const file = await open("file.txt", { read: true, dir: BaseDirectory.App }); + * const fileInfo = await fstat(file.rid); + * console.log(fileInfo.isFile); // true + * ``` + * + * @since 2.0.0 + */ + async stat(): Promise { + const res = await invoke("plugin:fs|fstat", { + rid: this.rid, + }); + + return parseFileInfo(res); + } + + /** + * Truncates or extends this file, to reach the specified `len`. + * If `len` is not specified then the entire file contents are truncated. + * + * @example + * ```typescript + * import { ftruncate, open, write, read, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * // truncate the entire file + * const file = await open("my_file.txt", { read: true, write: true, create: true, dir: BaseDirectory.App }); + * await ftruncate(file.rid); + * + * // truncate part of the file + * const file = await open("my_file.txt", { read: true, write: true, create: true, dir: BaseDirectory.App }); + * await write(file.rid, new TextEncoder().encode("Hello World")); + * await ftruncate(file.rid, 7); + * const data = new Uint8Array(32); + * await read(file.rid, data); + * console.log(new TextDecoder().decode(data)); // Hello W + * ``` + * + * @since 2.0.0 + */ + async truncate(len?: number): Promise { + return invoke("plugin:fs|ftruncate", { + rid: this.rid, + len, + }); + } + + /** + * Writes `p.byteLength` bytes from `p` to the underlying data stream. It + * resolves to the number of bytes written from `p` (`0` <= `n` <= + * `p.byteLength`) or reject with the error encountered that caused the + * write to stop early. `write()` must reject with a non-null error if + * would resolve to `n` < `p.byteLength`. `write()` must not modify the + * slice data, even temporarily. + * + * @example + * ```typescript + * import { open, write, close, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const file = await open("bar.txt", { write: true, dir: BaseDirectory.App }); + * const bytesWritten = await write(file.rid, data); // 11 + * await close(file.rid); + * ``` + * + * @since 2.0.0 + */ + async write(data: Uint8Array): Promise { + return invoke("plugin:fs|write", { + rid: this.rid, + data: Array.from(data), + }); + } } /** * @since 2.0.0 */ -interface FsDirOptions { - dir?: BaseDirectory; - recursive?: boolean; +interface CreateOptions { + /** Base directory for `path` */ + baseDir?: BaseDirectory; } /** - * Options object used to write a UTF-8 string to a file. + * Creates a file if none exists or truncates an existing file and resolves to + * an instance of {@linkcode FileHandle }. + * + * @example + * ```typescript + * import { create, BaseDirectory } from "@tauri-apps/plugin-fs" + * const file = await create("foo/bar.txt", { dir: BaseDirectory.App }); + * ``` * * @since 2.0.0 */ -interface FsTextFileOption { - /** Path to the file to write. */ - path: string; - /** The UTF-8 string to write to the file. */ - contents: string; -} +async function create( + path: string | URL, + options?: CreateOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } -type BinaryFileContents = Iterable | ArrayLike | ArrayBuffer; + const rid = await invoke("plugin:fs|create", { + path: path instanceof URL ? path.toString() : path, + options, + }); -/** - * Options object used to write a binary data to a file. - * - * @since 2.0.0 - */ -interface FsBinaryFileOption { - /** Path to the file to write. */ - path: string; - /** The byte array contents. */ - contents: BinaryFileContents; + return new FileHandle(rid); } /** * @since 2.0.0 */ -interface FileEntry { - path: string; +interface OpenOptions { /** - * Name of the directory/file - * can be null if the path terminates with `..` + * Sets the option for read access. This option, when `true`, means that the + * file should be read-able if opened. */ - name?: string; - /** Children of this entry if it's a directory; null otherwise */ - children?: FileEntry[]; + read?: boolean; + /** + * Sets the option for write access. This option, when `true`, means that + * the file should be write-able if opened. If the file already exists, + * any write calls on it will overwrite its contents, by default without + * truncating it. + */ + write?: boolean; + /** + * 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?: boolean; + /** + * 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` + * size if it already exists. The file must be opened with write access + * for truncate to work. + */ + truncate?: boolean; + /** + * Sets the option to allow creating a new file, if one doesn't already + * exist at the specified path. Requires write or append access to be + * used. + */ + create?: boolean; + /** + * Defaults to `false`. If set to `true`, no file, directory, or symlink is + * allowed to exist at the target location. Requires write or append + * access to be used. When createNew is set to `true`, create and truncate + * are ignored. + */ + createNew?: boolean; + /** + * Permissions to use if creating the file (defaults to `0o666`, before + * the process's umask). + * Ignored on Windows. + */ + mode?: number; + /** Base directory for `path` */ + baseDir?: BaseDirectory; } /** - * Reads a file as an UTF-8 encoded string. + * Open a file and resolve to an instance of {@linkcode FileHandle}. The + * file does not need to previously exist if using the `create` or `createNew` + * open options. It is the callers responsibility to close the file when finished + * with it. + * * @example * ```typescript - * import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Read the text file in the `$APPCONFIG/app.conf` path - * const contents = await readTextFile('app.conf', { dir: BaseDirectory.AppConfig }); + * import { open, BaseDirectory } from "@tauri-apps/plugin-fs" + * const file = await open("foo/bar.txt", { read: true, write: true, dir: BaseDirectory.App }); + * // Do work with file + * await close(file.rid); * ``` * * @since 2.0.0 */ -async function readTextFile( - filePath: string, - options: FsOptions = {}, -): Promise { - return await invoke("plugin:fs|read_text_file", { - path: filePath, +async function open( + path: string | URL, + options?: OpenOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + const rid = await invoke("plugin:fs|open", { + path: path instanceof URL ? path.toString() : path, options, }); + + return new FileHandle(rid); +} + +/** + * @since 2.0.0 + */ +interface CopyFileOptions { + /** Base directory for `fromPath`. */ + fromPathBaseDir?: BaseDirectory; + /** Base directory for `toPath`. */ + toPathBaseDir?: BaseDirectory; } /** - * Reads a file as byte array. + * Copies the contents and permissions of one file to another specified path, by default creating a new file if needed, else overwriting. * @example * ```typescript - * import { readBinaryFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Read the image file in the `$RESOURCEDIR/avatar.png` path - * const contents = await readBinaryFile('avatar.png', { dir: BaseDirectory.Resource }); + * import { copyFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * await copyFile('app.conf', 'app.conf.bk', { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ -async function readBinaryFile( - filePath: string, - options: FsOptions = {}, -): Promise { - const arr = await invoke("plugin:fs|read_file", { - path: filePath, +async function copyFile( + fromPath: string | URL, + toPath: string | URL, + options?: CopyFileOptions, +): Promise { + if ( + (fromPath instanceof URL && fromPath.protocol !== "file:") || + (toPath instanceof URL && toPath.protocol !== "file:") + ) { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|copy_file", { + fromPath: fromPath instanceof URL ? fromPath.toString() : fromPath, + toPath: toPath instanceof URL ? toPath.toString() : toPath, options, }); +} - return Uint8Array.from(arr); +/** + * @since 2.0.0 + */ +interface MkdirOptions { + /** Permissions to use when creating the directory (defaults to `0o777`, before the process's umask). Ignored on Windows. */ + mode?: number; + /** + * Defaults to `false`. If set to `true`, means that any intermediate directories will also be created (as with the shell command `mkdir -p`). + * */ + recursive?: boolean; + /** Base directory for `path` */ + baseDir?: BaseDirectory; } /** - * Writes a UTF-8 text file. + * Creates a new directory with the specified path. * @example * ```typescript - * import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Write a text file to the `$APPCONFIG/app.conf` path - * await writeTextFile('app.conf', 'file contents', { dir: BaseDirectory.AppConfig }); + * import { mkdir, BaseDirectory } from '@tauri-apps/plugin-fs'; + * await mkdir('users', { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ -async function writeTextFile( - path: string, - contents: string, - options?: FsOptions, -): Promise; +async function mkdir( + path: string | URL, + options?: MkdirOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|mkdir", { + path: path instanceof URL ? path.toString() : path, + options, + }); +} /** - * Writes a UTF-8 text file. - * @example - * ```typescript - * import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Write a text file to the `$APPCONFIG/app.conf` path - * await writeTextFile({ path: 'app.conf', contents: 'file contents' }, { dir: BaseDirectory.AppConfig }); - * ``` - * @returns A promise indicating the success or failure of the operation. - * * @since 2.0.0 */ -async function writeTextFile( - file: FsTextFileOption, - options?: FsOptions, -): Promise; +interface ReadDirOptions { + /** Base directory for `path` */ + baseDir?: BaseDirectory; +} /** - * Writes a UTF-8 text file. + * A disk entry which is either a file, a directory or a symlink. * - * @returns A promise indicating the success or failure of the operation. + * This is the result of the {@linkcode readDir}. * * @since 2.0.0 */ -async function writeTextFile( - path: string | FsTextFileOption, - contents?: string | FsOptions, - options?: FsOptions, -): Promise { - if (typeof options === "object") { - Object.freeze(options); - } - if (typeof path === "object") { - Object.freeze(path); - } - - const file: FsTextFileOption = { path: "", contents: "" }; - let fileOptions: FsOptions | undefined = options; - if (typeof path === "string") { - file.path = path; - } else { - file.path = path.path; - file.contents = path.contents; - } +interface DirEntry { + /** The name of the entry (file name with extension or directory name). */ + name: string; + /** Specifies whether this entry is a directory or not. */ + isDirectory: boolean; + /** Specifies whether this entry is a file or not. */ + isFile: boolean; + /** Specifies whether this entry is a symlink or not. */ + isSymlink: boolean; +} - if (typeof contents === "string") { - file.contents = contents ?? ""; - } else { - fileOptions = contents; +/** + * Reads the directory given by path and returns an array of `DirEntry`. + * @example + * ```typescript + * import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const dir = "users" + * const entries = await readDir('users', { dir: BaseDirectory.App }); + * processEntriesRecursive(dir, entries); + * async function processEntriesRecursive(parent, entries) { + * for (const entry of entries) { + * console.log(`Entry: ${entry.name}`); + * if (entry.isDirectory) { + * const dir = parent + entry.name; + * processEntriesRecursive(dir, await readDir(dir, { dir: BaseDirectory.App })) + * } + * } + * } + * ``` + * + * @since 2.0.0 + */ +async function readDir( + path: string | URL, + options?: ReadDirOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); } - return await invoke("plugin:fs|write_file", { - path: file.path, - contents: Array.from(new TextEncoder().encode(file.contents)), - options: fileOptions, + return invoke("plugin:fs|read_dir", { + path: path instanceof URL ? path.toString() : path, + options, }); } /** - * Writes a byte array content to a file. + * @since 2.0.0 + */ +interface ReadFileOptions { + /** Base directory for `path` */ + baseDir?: BaseDirectory; +} + +/** + * Reads and resolves to the entire contents of a file as an array of bytes. + * TextDecoder can be used to transform the bytes to string if required. * @example * ```typescript - * import { writeBinaryFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Write a binary file to the `$APPDATA/avatar.png` path - * await writeBinaryFile('avatar.png', new Uint8Array([]), { dir: BaseDirectory.AppData }); + * import { readFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const contents = await readFile('avatar.png', { dir: BaseDirectory.Resource }); * ``` * - * @param options Configuration object. - * @returns A promise indicating the success or failure of the operation. - * * @since 2.0.0 */ -async function writeBinaryFile( - path: string, - contents: BinaryFileContents, - options?: FsOptions, -): Promise; +async function readFile( + path: string | URL, + options?: ReadFileOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + const arr = await invoke("plugin:fs|read_file", { + path: path instanceof URL ? path.toString() : path, + options, + }); + + return Uint8Array.from(arr); +} /** - * Writes a byte array content to a file. + * Reads and returns the entire contents of a file as UTF-8 string. * @example * ```typescript - * import { writeBinaryFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Write a binary file to the `$APPDATA/avatar.png` path - * await writeBinaryFile({ path: 'avatar.png', contents: new Uint8Array([]) }, { dir: BaseDirectory.AppData }); + * import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const contents = await readTextFile('app.conf', { dir: BaseDirectory.App }); * ``` * - * @param file The object containing the file path and contents. - * @param options Configuration object. - * @returns A promise indicating the success or failure of the operation. - * * @since 2.0.0 */ -async function writeBinaryFile( - file: FsBinaryFileOption, - options?: FsOptions, -): Promise; +async function readTextFile( + path: string | URL, + options?: ReadFileOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|read_text_file", { + path: path instanceof URL ? path.toString() : path, + options, + }); +} /** - * Writes a byte array content to a file. - * - * @returns A promise indicating the success or failure of the operation. + * Returns an async {@linkcode AsyncIterableIterator} over the lines of a file as UTF-8 string. + * @example + * ```typescript + * import { readTextFileLines, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const lines = await readTextFileLines('app.conf', { dir: BaseDirectory.App }); + * for await (const line of lines) { + * console.log(line); + * } + * ``` + * You could also call {@linkcode AsyncIterableIterator.next} to advance the + * iterator so you can lazily read the next line whenever you want. * * @since 2.0.0 */ -async function writeBinaryFile( - path: string | FsBinaryFileOption, - contents?: BinaryFileContents | FsOptions, - options?: FsOptions, -): Promise { - if (typeof options === "object") { - Object.freeze(options); - } - if (typeof path === "object") { - Object.freeze(path); +async function readTextFileLines( + path: string | URL, + options?: ReadFileOptions, +): Promise> { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); } - const file: FsBinaryFileOption = { path: "", contents: [] }; - let fileOptions: FsOptions | undefined = options; - if (typeof path === "string") { - file.path = path; - } else { - file.path = path.path; - file.contents = path.contents; - } + const pathStr = path instanceof URL ? path.toString() : path; - if (contents && "dir" in contents) { - fileOptions = contents; - } else if (typeof path === "string") { - // @ts-expect-error in this case `contents` is always a BinaryFileContents - file.contents = contents ?? []; - } + return Promise.resolve({ + path: pathStr, + rid: null as number | null, + async next(): Promise> { + if (!this.rid) { + this.rid = await invoke("plugin:fs|read_text_file_lines", { + path: pathStr, + options, + }); + } + + const [line, done] = await invoke<[string | null, boolean]>( + "plugin:fs|read_text_file_lines_next", + { rid: this.rid }, + ); - return await invoke("plugin:fs|write_file", { - path: file.path, - contents: Array.from( - file.contents instanceof ArrayBuffer - ? new Uint8Array(file.contents) - : file.contents, - ), - options: fileOptions, + // an iteration is over, reset rid for next iteration + if (done) this.rid = null; + + return { + value: done ? "" : (line as string), + done, + }; + }, + [Symbol.asyncIterator](): AsyncIterableIterator { + return this; + }, }); } /** - * List directory files. + * @since 2.0.0 + */ +interface RemoveOptions { + /** Defaults to `false`. If set to `true`, path will be removed even if it's a non-empty directory. */ + recursive?: boolean; + /** Base directory for `path` */ + baseDir?: BaseDirectory; +} + +/** + * Removes the named file or directory. + * If the directory is not empty and the `recursive` option isn't set to true, the promise will be rejected. * @example * ```typescript - * import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Reads the `$APPDATA/users` directory recursively - * const entries = await readDir('users', { dir: BaseDirectory.AppData, recursive: true }); - * - * function processEntries(entries) { - * for (const entry of entries) { - * console.log(`Entry: ${entry.path}`); - * if (entry.children) { - * processEntries(entry.children) - * } - * } - * } + * import { remove, BaseDirectory } from '@tauri-apps/plugin-fs'; + * await remove('users/file.txt', { dir: BaseDirectory.App }); + * await remove('users', { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ -async function readDir( - dir: string, - options: FsDirOptions = {}, -): Promise { - return await invoke("plugin:fs|read_dir", { - path: dir, +async function remove( + path: string | URL, + options?: RemoveOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|remove", { + path: path instanceof URL ? path.toString() : path, options, }); } /** - * Creates a directory. - * If one of the path's parent components doesn't exist - * and the `recursive` option isn't set to true, the promise will be rejected. + * @since 2.0.0 + */ +interface RenameOptions { + /** Base directory for `oldPath`. */ + oldPathBaseDir?: BaseDirectory; + /** Base directory for `newPath`. */ + newPathBaseDir?: BaseDirectory; +} + +/** + * Renames (moves) oldpath to newpath. Paths may be files or directories. + * If newpath already exists and is not a directory, rename() replaces it. + * OS-specific restrictions may apply when oldpath and newpath are in different directories. + * + * On Unix, this operation does not follow symlinks at either path. + * * @example * ```typescript - * import { createDir, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Create the `$APPDATA/users` directory - * await createDir('users', { dir: BaseDirectory.AppData, recursive: true }); + * import { rename, BaseDirectory } from '@tauri-apps/plugin-fs'; + * await rename('avatar.png', 'deleted.png', { dir: BaseDirectory.App }); * ``` * - * @returns A promise indicating the success or failure of the operation. - * * @since 2.0.0 */ -async function createDir( - dir: string, - options: FsDirOptions = {}, +async function rename( + oldPath: string | URL, + newPath: string | URL, + options: RenameOptions, ): Promise { - return await invoke("plugin:fs|create_dir", { - path: dir, + if ( + (oldPath instanceof URL && oldPath.protocol !== "file:") || + (newPath instanceof URL && newPath.protocol !== "file:") + ) { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|rename", { + oldPath: oldPath instanceof URL ? oldPath.toString() : oldPath, + newPath: newPath instanceof URL ? newPath.toString() : newPath, options, }); } /** - * Removes a directory. - * If the directory is not empty and the `recursive` option isn't set to true, the promise will be rejected. + * @since 2.0.0 + */ +interface StatOptions { + /** Base directory for `path`. */ + baseDir?: BaseDirectory; +} + +/** + * Resolves to a {@linkcode FileInfo} for the specified `path`. Will always + * follow symlinks but will reject if the symlink points to a path outside of the scope. + * * @example * ```typescript - * import { removeDir, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Remove the directory `$APPDATA/users` - * await removeDir('users', { dir: BaseDirectory.AppData }); + * import { stat, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const fileInfo = await stat("hello.txt", { dir: BaseDirectory.App }); + * console.log(fileInfo.isFile); // true * ``` * - * @returns A promise indicating the success or failure of the operation. - * * @since 2.0.0 */ -async function removeDir( - dir: string, - options: FsDirOptions = {}, -): Promise { - return await invoke("plugin:fs|remove_dir", { - path: dir, +async function stat( + path: string | URL, + options?: StatOptions, +): Promise { + const res = await invoke("plugin:fs|stat", { + path: path instanceof URL ? path.toString() : path, options, }); + + return parseFileInfo(res); } /** - * Copies a file to a destination. + * Resolves to a {@linkcode FileInfo} for the specified `path`. If `path` is a + * symlink, information for the symlink will be returned instead of what it + * points to. + * * @example * ```typescript - * import { copyFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Copy the `$APPCONFIG/app.conf` file to `$APPCONFIG/app.conf.bk` - * await copyFile('app.conf', 'app.conf.bk', { dir: BaseDirectory.AppConfig }); + * import { lstat, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const fileInfo = await lstat("hello.txt", { dir: BaseDirectory.App }); + * console.log(fileInfo.isFile); // true * ``` * - * @returns A promise indicating the success or failure of the operation. - * * @since 2.0.0 */ -async function copyFile( - source: string, - destination: string, - options: FsOptions = {}, -): Promise { - return await invoke("plugin:fs|copy_file", { - source, - destination, +async function lstat( + path: string | URL, + options?: StatOptions, +): Promise { + const res = await invoke("plugin:fs|lstat", { + path: path instanceof URL ? path.toString() : path, options, }); + + return parseFileInfo(res); } /** - * Removes a file. + * @since 2.0.0 + */ +interface TruncateOptions { + /** Base directory for `path`. */ + baseDir?: BaseDirectory; +} + +/** + * Truncates or extends the specified file, to reach the specified `len`. + * If `len` is `0` or not specified, then the entire file contents are truncated. + * * @example * ```typescript - * import { removeFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Remove the `$APPConfig/app.conf` file - * await removeFile('app.conf', { dir: BaseDirectory.AppConfig }); - * ``` + * import { truncate, readFile, writeFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * // truncate the entire file + * await truncate("my_file.txt", 0, { dir: BaseDirectory.App }); * - * @returns A promise indicating the success or failure of the operation. + * // truncate part of the file + * let file = "file.txt"; + * await writeFile(file, new TextEncoder().encode("Hello World"), { dir: BaseDirectory.App }); + * await truncate(file, 7); + * const data = await readFile(file, { dir: BaseDirectory.App }); + * console.log(new TextDecoder().decode(data)); // "Hello W" + * ``` * * @since 2.0.0 */ -async function removeFile( - file: string, - options: FsOptions = {}, +async function truncate( + path: string | URL, + len?: number, + options?: TruncateOptions, ): Promise { - return await invoke("plugin:fs|remove_file", { - path: file, + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|truncate", { + path: path instanceof URL ? path.toString() : path, + len, options, }); } /** - * Renames a file. + * @since 2.0.0 + */ +interface WriteFileOptions { + /** Defaults to `false`. If set to `true`, will append to a file instead of overwriting previous contents. */ + append?: boolean; + /** Sets the option to allow creating a new file, if one doesn't already exist at the specified path (defaults to `true`). */ + create?: boolean; + /** File permissions. Ignored on Windows. */ + mode?: number; + /** Base directory for `path` */ + baseDir?: BaseDirectory; +} + +/** + * Write `data` to the given `path`, by default creating a new file if needed, else overwriting. * @example * ```typescript - * import { renameFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * // Rename the `$APPDATA/avatar.png` file - * await renameFile('avatar.png', 'deleted.png', { dir: BaseDirectory.AppData }); - * ``` + * import { writeFile, BaseDirectory } from '@tauri-apps/plugin-fs'; * - * @returns A promise indicating the success or failure of the operation. + * let encoder = new TextEncoder(); + * let data = encoder.encode("Hello World"); + * await writeFile('file.txt', data, { dir: BaseDirectory.App }); + * ``` * * @since 2.0.0 */ -async function renameFile( - oldPath: string, - newPath: string, - options: FsOptions = {}, +async function writeFile( + path: string | URL, + data: Uint8Array, + options?: WriteFileOptions, ): Promise { - return await invoke("plugin:fs|rename_file", { - oldPath, - newPath, + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|write_file", { + path: path instanceof URL ? path.toString() : path, + data: Array.from(data), options, }); } +/** + * Writes UTF-8 string `data` to the given `path`, by default creating a new file if needed, else overwriting. + @example + * ```typescript + * import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * await writeTextFile('file.txt', "Hello world", { dir: BaseDirectory.App }); + * ``` + * + * @since 2.0.0 + */ +async function writeTextFile( + path: string | URL, + data: string, + options?: WriteFileOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|write_text_file", { + path: path instanceof URL ? path.toString() : path, + data, + options, + }); +} + +/** + * @since 2.0.0 + */ +interface ExistsOptions { + /** Base directory for `path`. */ + baseDir?: BaseDirectory; +} + /** * Check if a path exists. * @example @@ -580,54 +1061,200 @@ async function renameFile( * * @since 2.0.0 */ -async function exists(path: string, options: FsOptions = {}): Promise { - return await invoke("plugin:fs|exists", { path, options }); +async function exists( + path: string | URL, + options?: ExistsOptions, +): Promise { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + + return invoke("plugin:fs|exists", { + path: path instanceof URL ? path.toString() : path, + options, + }); +} + +/** + * @since 2.0.0 + */ +interface WatchOptions { + /** Watch a directory recursively */ + recursive?: boolean; + /** Base directory for `path` */ + baseDir?: BaseDirectory; +} + +/** + * @since 2.0.0 + */ +interface DebouncedWatchOptions extends WatchOptions { + /** Debounce delay */ + delayMs?: number; +} + +/** + * @since 2.0.0 + */ +type RawEvent = { + type: RawEventKind; + paths: string[]; + attrs: unknown; +}; + +/** + * @since 2.0.0 + */ +type RawEventKind = + | "any " + | { + access?: unknown; + } + | { + create?: unknown; + } + | { + modify?: unknown; + } + | { + remove?: unknown; + } + | "other"; + +/** + * @since 2.0.0 + */ +type DebouncedEvent = + | { kind: "any"; path: string } + | { kind: "AnyContinous"; path: string }; + +/** + * @since 2.0.0 + */ +type UnwatchFn = () => void; + +async function unwatch(id: number): Promise { + await invoke("plugin:fs|unwatch", { id }); +} + +/** + * Watch changes (after a delay) on files or directories. + * + * @since 2.0.0 + */ +async function watch( + paths: string | string[] | URL | URL[], + cb: (event: DebouncedEvent) => void, + options?: DebouncedWatchOptions, +): Promise { + const opts = { + recursive: false, + delayMs: 2000, + ...options, + }; + + const watchPaths = Array.isArray(paths) ? paths : [paths]; + + for (const path of watchPaths) { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + } + + const onEvent = new Channel(); + onEvent.onmessage = cb; + + const rid: number = await invoke("plugin:fs|watch", { + paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)), + options: opts, + onEvent, + }); + + return () => { + void unwatch(rid); + }; } /** - * Returns the metadata for the given path. + * Watch changes on files or directories. * * @since 2.0.0 */ -async function metadata(path: string): Promise { - return await invoke("plugin:fs|metadata", { - path, - }).then((metadata) => { - const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata; - return { - accessedAt: new Date(accessedAtMs), - createdAt: new Date(createdAtMs), - modifiedAt: new Date(modifiedAtMs), - ...data, - }; +async function watchImmediate( + paths: string | string[] | URL | URL[], + cb: (event: RawEvent) => void, + options?: WatchOptions, +): Promise { + const opts = { + recursive: false, + ...options, + delayMs: null, + }; + + const watchPaths = Array.isArray(paths) ? paths : [paths]; + + for (const path of watchPaths) { + if (path instanceof URL && path.protocol !== "file:") { + throw new TypeError("Must be a file URL."); + } + } + + const onEvent = new Channel(); + onEvent.onmessage = cb; + + const rid: number = await invoke("plugin:fs|watch", { + paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)), + options: opts, + onEvent, }); + + return () => { + void unwatch(rid); + }; } export type { - FsOptions, - FsDirOptions, - FsTextFileOption, - BinaryFileContents, - FsBinaryFileOption, - FileEntry, - Permissions, - Metadata, + CreateOptions, + OpenOptions, + CopyFileOptions, + MkdirOptions, + DirEntry, + ReadDirOptions, + ReadFileOptions, + RemoveOptions, + RenameOptions, + StatOptions, + TruncateOptions, + WriteFileOptions, + ExistsOptions, + FileInfo, + WatchOptions, + DebouncedWatchOptions, + DebouncedEvent, + RawEvent, + UnwatchFn, }; export { BaseDirectory, - BaseDirectory as Dir, + FileHandle, + create, + open, + copyFile, + mkdir, + readDir, + readFile, readTextFile, - readBinaryFile, + readTextFileLines, + remove, + rename, + SeekMode, + stat, + lstat, + truncate, + writeFile, writeTextFile, - writeTextFile as writeFile, - writeBinaryFile, - readDir, - createDir, - removeDir, - copyFile, - removeFile, - renameFile, exists, - metadata, + watch, + watchImmediate, }; diff --git a/plugins/fs/src/api-iife.js b/plugins/fs/src/api-iife.js index 2bf92834..ad0a0013 100644 --- a/plugins/fs/src/api-iife.js +++ b/plugins/fs/src/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(t){"use strict";async function e(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}var n;async function a(t,n,a){"object"==typeof a&&Object.freeze(a),"object"==typeof t&&Object.freeze(t);const i={path:"",contents:""};let o=a;return"string"==typeof t?i.path=t:(i.path=t.path,i.contents=t.contents),"string"==typeof n?i.contents=n??"":o=n,await e("plugin:fs|write_file",{path:i.path,contents:Array.from((new TextEncoder).encode(i.contents)),options:o})}return"function"==typeof SuppressedError&&SuppressedError,t.Dir=void 0,(n=t.Dir||(t.Dir={}))[n.Audio=1]="Audio",n[n.Cache=2]="Cache",n[n.Config=3]="Config",n[n.Data=4]="Data",n[n.LocalData=5]="LocalData",n[n.Document=6]="Document",n[n.Download=7]="Download",n[n.Picture=8]="Picture",n[n.Public=9]="Public",n[n.Video=10]="Video",n[n.Resource=11]="Resource",n[n.Temp=12]="Temp",n[n.AppConfig=13]="AppConfig",n[n.AppData=14]="AppData",n[n.AppLocalData=15]="AppLocalData",n[n.AppCache=16]="AppCache",n[n.AppLog=17]="AppLog",n[n.Desktop=18]="Desktop",n[n.Executable=19]="Executable",n[n.Font=20]="Font",n[n.Home=21]="Home",n[n.Runtime=22]="Runtime",n[n.Template=23]="Template",t.BaseDirectory=t.Dir,t.copyFile=async function(t,n,a={}){return await e("plugin:fs|copy_file",{source:t,destination:n,options:a})},t.createDir=async function(t,n={}){return await e("plugin:fs|create_dir",{path:t,options:n})},t.exists=async function(t,n={}){return await e("plugin:fs|exists",{path:t,options:n})},t.metadata=async function(t){return await e("plugin:fs|metadata",{path:t}).then((t=>{const{accessedAtMs:e,createdAtMs:n,modifiedAtMs:a,...i}=t;return{accessedAt:new Date(e),createdAt:new Date(n),modifiedAt:new Date(a),...i}}))},t.readBinaryFile=async function(t,n={}){const a=await e("plugin:fs|read_file",{path:t,options:n});return Uint8Array.from(a)},t.readDir=async function(t,n={}){return await e("plugin:fs|read_dir",{path:t,options:n})},t.readTextFile=async function(t,n={}){return await e("plugin:fs|read_text_file",{path:t,options:n})},t.removeDir=async function(t,n={}){return await e("plugin:fs|remove_dir",{path:t,options:n})},t.removeFile=async function(t,n={}){return await e("plugin:fs|remove_file",{path:t,options:n})},t.renameFile=async function(t,n,a={}){return await e("plugin:fs|rename_file",{oldPath:t,newPath:n,options:a})},t.writeBinaryFile=async function(t,n,a){"object"==typeof a&&Object.freeze(a),"object"==typeof t&&Object.freeze(t);const i={path:"",contents:[]};let o=a;return"string"==typeof t?i.path=t:(i.path=t.path,i.contents=t.contents),n&&"dir"in n?o=n:"string"==typeof t&&(i.contents=n??[]),await e("plugin:fs|write_file",{path:i.path,contents:Array.from(i.contents instanceof ArrayBuffer?new Uint8Array(i.contents):i.contents),options:o})},t.writeFile=a,t.writeTextFile=a,t}({});Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(t){"use strict";function e(t,e,n,i){if("a"===n&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?i:"a"===n?i.call(t):i?i.value:e.get(t)}function n(t,e,n,i,o){if("m"===i)throw new TypeError("Private method is not writable");if("a"===i&&!o)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof e?t!==e||!o:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===i?o.call(t,n):o?o.value=n:e.set(t,n),n}var i,o,r,a;"function"==typeof SuppressedError&&SuppressedError;class s{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,i.set(this,(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((t=>{e(this,i,"f").call(this,t)}))}set onmessage(t){n(this,i,t,"f")}get onmessage(){return e(this,i,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function c(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}i=new WeakMap;class f{get rid(){return e(this,o,"f")}constructor(t){o.set(this,void 0),n(this,o,t,"f")}async close(){return c("plugin:resources|close",{rid:this.rid})}}function l(t){return{isFile:t.isFile,isDirectory:t.isDirectory,isSymlink:t.isSymlink,size:t.size,mtime:null!=t.mtime?new Date(t.mtime):null,atime:null!=t.atime?new Date(t.atime):null,birthtime:null!=t.birthtime?new Date(t.birthtime):null,readonly:t.readonly,fileAttributes:t.fileAttributes,dev:t.dev,ino:t.ino,mode:t.mode,nlink:t.nlink,uid:t.uid,gid:t.gid,rdev:t.rdev,blksize:t.blksize,blocks:t.blocks}}o=new WeakMap,t.BaseDirectory=void 0,(r=t.BaseDirectory||(t.BaseDirectory={}))[r.Audio=1]="Audio",r[r.Cache=2]="Cache",r[r.Config=3]="Config",r[r.Data=4]="Data",r[r.LocalData=5]="LocalData",r[r.Document=6]="Document",r[r.Download=7]="Download",r[r.Picture=8]="Picture",r[r.Public=9]="Public",r[r.Video=10]="Video",r[r.Resource=11]="Resource",r[r.Temp=12]="Temp",r[r.AppConfig=13]="AppConfig",r[r.AppData=14]="AppData",r[r.AppLocalData=15]="AppLocalData",r[r.AppCache=16]="AppCache",r[r.AppLog=17]="AppLog",r[r.Desktop=18]="Desktop",r[r.Executable=19]="Executable",r[r.Font=20]="Font",r[r.Home=21]="Home",r[r.Runtime=22]="Runtime",r[r.Template=23]="Template",t.SeekMode=void 0,(a=t.SeekMode||(t.SeekMode={}))[a.Start=0]="Start",a[a.Current=1]="Current",a[a.End=2]="End";class u extends f{constructor(t){super(t)}async read(t){if(0===t.byteLength)return 0;const[e,n]=await c("plugin:fs|read",{rid:this.rid,len:t.byteLength});return t.set(e),0===n?null:n}async seek(t,e){return c("plugin:fs|seek",{rid:this.rid,offset:t,whence:e})}async stat(){return l(await c("plugin:fs|fstat",{rid:this.rid}))}async truncate(t){return c("plugin:fs|ftruncate",{rid:this.rid,len:t})}async write(t){return c("plugin:fs|write",{rid:this.rid,data:Array.from(t)})}}async function p(t){await c("plugin:fs|unwatch",{id:t})}return t.FileHandle=u,t.copyFile=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol||e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|copy_file",{fromPath:t instanceof URL?t.toString():t,toPath:e instanceof URL?e.toString():e,options:n})},t.create=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const n=await c("plugin:fs|create",{path:t instanceof URL?t.toString():t,options:e});return new u(n)},t.exists=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|exists",{path:t instanceof URL?t.toString():t,options:e})},t.lstat=async function(t,e){return l(await c("plugin:fs|lstat",{path:t instanceof URL?t.toString():t,options:e}))},t.mkdir=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|mkdir",{path:t instanceof URL?t.toString():t,options:e})},t.open=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const n=await c("plugin:fs|open",{path:t instanceof URL?t.toString():t,options:e});return new u(n)},t.readDir=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|read_dir",{path:t instanceof URL?t.toString():t,options:e})},t.readFile=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const n=await c("plugin:fs|read_file",{path:t instanceof URL?t.toString():t,options:e});return Uint8Array.from(n)},t.readTextFile=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|read_text_file",{path:t instanceof URL?t.toString():t,options:e})},t.readTextFileLines=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const n=t instanceof URL?t.toString():t;return Promise.resolve({path:n,rid:null,async next(){this.rid||(this.rid=await c("plugin:fs|read_text_file_lines",{path:n,options:e}));const[t,i]=await c("plugin:fs|read_text_file_lines_next",{rid:this.rid});return i&&(this.rid=null),{value:i?"":t,done:i}},[Symbol.asyncIterator](){return this}})},t.remove=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|remove",{path:t instanceof URL?t.toString():t,options:e})},t.rename=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol||e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|rename",{oldPath:t instanceof URL?t.toString():t,newPath:e instanceof URL?e.toString():e,options:n})},t.stat=async function(t,e){return l(await c("plugin:fs|stat",{path:t instanceof URL?t.toString():t,options:e}))},t.truncate=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|truncate",{path:t instanceof URL?t.toString():t,len:e,options:n})},t.watch=async function(t,e,n){const i={recursive:!1,delayMs:2e3,...n},o=Array.isArray(t)?t:[t];for(const t of o)if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const r=new s;r.onmessage=e;const a=await c("plugin:fs|watch",{paths:o.map((t=>t instanceof URL?t.toString():t)),options:i,onEvent:r});return()=>{p(a)}},t.watchImmediate=async function(t,e,n){const i={recursive:!1,...n,delayMs:null},o=Array.isArray(t)?t:[t];for(const t of o)if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const r=new s;r.onmessage=e;const a=await c("plugin:fs|watch",{paths:o.map((t=>t instanceof URL?t.toString():t)),options:i,onEvent:r});return()=>{p(a)}},t.writeFile=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|write_file",{path:t instanceof URL?t.toString():t,data:Array.from(e),options:n})},t.writeTextFile=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return c("plugin:fs|write_text_file",{path:t instanceof URL?t.toString():t,data:e,options:n})},t}({});Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index ac0893a7..bde9a9ea 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -1,27 +1,24 @@ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// Copyright 2018-2023 the Deno authors. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::Scope; -use anyhow::Context; use serde::{Deserialize, Serialize, Serializer}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use tauri::{ path::{BaseDirectory, SafePathBuf}, - Manager, Runtime, Window, + AppHandle, Manager, Resource, ResourceId, Runtime, }; -#[cfg(unix)] -use std::os::unix::fs::{MetadataExt, PermissionsExt}; -#[cfg(windows)] -use std::os::windows::fs::MetadataExt; use std::{ - fs::{self, symlink_metadata, File}, - io::Write, + fs::File, + io::{BufReader, Lines, Read, Write}, path::{Path, PathBuf}, + sync::Mutex, time::{SystemTime, UNIX_EPOCH}, }; -use crate::{Error, FsExt, Result}; +use crate::{Error, FsExt}; #[derive(Debug, thiserror::Error)] pub enum CommandError { @@ -29,6 +26,25 @@ pub enum CommandError { Anyhow(#[from] anyhow::Error), #[error(transparent)] Plugin(#[from] Error), + #[error(transparent)] + Tauri(#[from] tauri::Error), + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + #[cfg(feature = "watch")] + #[error(transparent)] + Watcher(#[from] notify::Error), +} + +impl From for CommandError { + fn from(value: String) -> Self { + Self::Anyhow(anyhow::anyhow!(value)) + } +} + +impl From<&str> for CommandError { + fn from(value: &str) -> Self { + Self::Anyhow(anyhow::anyhow!(value.to_string())) + } } impl Serialize for CommandError { @@ -44,360 +60,750 @@ impl Serialize for CommandError { } } -type CommandResult = std::result::Result; +pub type CommandResult = std::result::Result; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BaseOptions { + base_dir: Option, +} + +#[tauri::command] +pub fn create( + app: AppHandle, + path: SafePathBuf, + options: Option, +) -> CommandResult { + let resolved_path = resolve_path(&app, path, options.and_then(|o| o.base_dir))?; + let file = File::create(&resolved_path).map_err(|e| { + format!( + "failed to create file at path: {} with error: {e}", + resolved_path.display() + ) + })?; + let rid = app.resources_table().add(StdFileResource::new(file)); + Ok(rid) +} -/// The options for the directory functions on the file system API. #[derive(Debug, Clone, Deserialize)] -pub struct DirOperationOptions { - /// Whether the API should recursively perform the operation on the directory. +#[serde(rename_all = "camelCase")] +pub struct OpenOptions { + #[serde(flatten)] + base: BaseOptions, + #[serde(default = "default_true")] + read: bool, + #[serde(default)] + write: bool, + #[serde(default)] + append: bool, #[serde(default)] - pub recursive: bool, - /// The base directory of the operation. - /// The directory path of the BaseDirectory will be the prefix of the defined directory path. - pub dir: Option, + truncate: bool, + #[serde(default)] + create: bool, + #[serde(default)] + create_new: bool, + #[allow(unused)] + mode: Option, +} + +fn default_true() -> bool { + true +} + +#[tauri::command] +pub fn open( + app: AppHandle, + path: SafePathBuf, + options: Option, +) -> CommandResult { + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base.base_dir))?; + + let mut opts = std::fs::OpenOptions::new(); + // 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); + } + } + + opts.read(options.read) + .create(options.create) + .write(options.write) + .truncate(options.truncate) + .append(options.append) + .create_new(options.create_new); + } + + let file = opts.open(&resolved_path).map_err(|e| { + format!( + "failed to open file at path: {} with error: {e}", + resolved_path.display() + ) + })?; + + let rid = app.resources_table().add(StdFileResource::new(file)); + + Ok(rid) +} + +#[tauri::command] +pub fn close(app: AppHandle, rid: ResourceId) -> CommandResult<()> { + app.resources_table().close(rid).map_err(Into::into) +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CopyFileOptions { + from_path_base_dir: Option, + to_path_base_dir: Option, +} + +#[tauri::command] +pub fn copy_file( + app: AppHandle, + from_path: SafePathBuf, + to_path: SafePathBuf, + options: Option, +) -> CommandResult<()> { + let resolved_from_path = resolve_path( + &app, + from_path, + options.as_ref().and_then(|o| o.from_path_base_dir), + )?; + let resolved_to_path = resolve_path( + &app, + to_path, + options.as_ref().and_then(|o| o.to_path_base_dir), + )?; + std::fs::copy(&resolved_from_path, &resolved_to_path).map_err(|e| { + format!( + "failed to copy file from path: {}, to path: {} with error: {e}", + resolved_from_path.display(), + resolved_to_path.display() + ) + })?; + Ok(()) } -/// The options for the file functions on the file system API. #[derive(Debug, Clone, Deserialize)] -pub struct FileOperationOptions { - /// The base directory of the operation. - /// The directory path of the BaseDirectory will be the prefix of the defined file path. - pub dir: Option, +pub struct MkdirOptions { + #[serde(flatten)] + base: BaseOptions, + #[allow(unused)] + mode: Option, + recursive: Option, } -fn resolve_path( - window: &Window, +#[tauri::command] +pub fn mkdir( + app: AppHandle, path: SafePathBuf, - dir: Option, -) -> Result { - let path = if let Some(dir) = dir { - window - .path() - .resolve(&path, dir) - .map_err(Error::CannotResolvePath)? - } else { - path.as_ref().to_path_buf() - }; - if window.fs_scope().is_allowed(&path) { - Ok(path) - } else { - Err(Error::PathForbidden(path)) + options: Option, +) -> CommandResult<()> { + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base.base_dir))?; + + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(options.as_ref().and_then(|o| o.recursive).unwrap_or(false)); + + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + let mode = options.as_ref().and_then(|o| o.mode).unwrap_or(0o777) & 0o777; + builder.mode(mode); } + + builder + .create(&resolved_path) + .map_err(|e| { + format!( + "failed to create directory at path: {} with error: {e}", + resolved_path.display() + ) + }) + .map_err(Into::into) +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DirEntry { + pub name: Option, + pub is_directory: bool, + pub is_file: bool, + pub is_symlink: bool, +} + +fn read_dir_inner>(path: P) -> crate::Result> { + let mut files_and_dirs: Vec = vec![]; + for entry in std::fs::read_dir(path)? { + let path = entry?.path(); + let file_type = path.metadata()?.file_type(); + files_and_dirs.push(DirEntry { + is_directory: file_type.is_dir(), + is_file: file_type.is_file(), + is_symlink: std::fs::symlink_metadata(&path) + .map(|md| md.file_type().is_symlink()) + .unwrap_or(false), + name: path + .file_name() + .map(|name| name.to_string_lossy()) + .map(|name| name.to_string()), + }); + } + Result::Ok(files_and_dirs) +} + +#[tauri::command] +pub fn read_dir( + app: AppHandle, + path: SafePathBuf, + options: Option, +) -> CommandResult> { + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base_dir))?; + + read_dir_inner(&resolved_path) + .map_err(|e| { + format!( + "failed to read directory at path: {} with error: {e}", + resolved_path.display() + ) + }) + .map_err(Into::into) +} + +#[tauri::command] +pub fn read( + app: AppHandle, + rid: ResourceId, + len: u32, +) -> CommandResult<(Vec, usize)> { + let mut data = vec![0; len as usize]; + let file = app.resources_table().get::(rid)?; + let nread = StdFileResource::with_lock(&file, |mut file| file.read(&mut data)) + .map_err(|e| format!("faied to read bytes from file with error: {e}"))?; + Ok((data, nread)) } #[tauri::command] pub fn read_file( - window: Window, + app: AppHandle, path: SafePathBuf, - options: Option, + options: Option, ) -> CommandResult> { - let resolved_path = resolve_path(&window, path, options.and_then(|o| o.dir))?; - fs::read(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display())) + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base_dir))?; + std::fs::read(&resolved_path) + .map_err(|e| { + format!( + "failed to read file at path: {} with error: {e}", + resolved_path.display() + ) + }) .map_err(Into::into) } #[tauri::command] pub fn read_text_file( - window: Window, + app: AppHandle, path: SafePathBuf, - options: Option, + options: Option, ) -> CommandResult { - let resolved_path = resolve_path(&window, path, options.and_then(|o| o.dir))?; - fs::read_to_string(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display())) + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base_dir))?; + std::fs::read_to_string(&resolved_path) + .map_err(|e| { + format!( + "failed to read file as text at path: {} with error: {e}", + resolved_path.display() + ) + }) .map_err(Into::into) } #[tauri::command] -pub fn write_file( - window: Window, +pub fn read_text_file_lines( + app: AppHandle, path: SafePathBuf, - contents: Vec, - options: Option, -) -> CommandResult<()> { - let resolved_path = resolve_path(&window, path, options.and_then(|o| o.dir))?; - File::create(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display())) - .map_err(Into::into) - .and_then(|mut f| { - f.write_all(&contents) - .map_err(|err| anyhow::anyhow!("{}", err)) - .map_err(Into::into) - }) + options: Option, +) -> CommandResult { + use std::io::BufRead; + + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base_dir))?; + + let file = File::open(&resolved_path).map_err(|e| { + format!( + "failed to open file at path: {} with error: {e}", + resolved_path.display() + ) + })?; + + let lines = BufReader::new(file).lines(); + let rid = app.resources_table().add(StdLinesResource::new(lines)); + + Ok(rid) } -#[derive(Clone, Copy)] -struct ReadDirOptions<'a> { - pub scope: Option<&'a Scope>, +#[tauri::command] +pub fn read_text_file_lines_next( + app: AppHandle, + rid: ResourceId, +) -> CommandResult<(Option, bool)> { + let mut resource_table = app.resources_table(); + let lines = resource_table.get::(rid)?; + + let ret = StdLinesResource::with_lock(&lines, |lines| { + lines.next().map(|a| (a.ok(), false)).unwrap_or_else(|| { + let _ = resource_table.close(rid); + (None, true) + }) + }); + + Ok(ret) } -#[derive(Debug, Serialize)] -#[non_exhaustive] -pub struct DiskEntry { - /// The path to the entry. - pub path: PathBuf, - /// The name of the entry (file name with extension or directory name). - pub name: Option, - /// The children of this entry if it's a directory. - #[serde(skip_serializing_if = "Option::is_none")] - pub children: Option>, -} - -fn read_dir_with_options>( - path: P, - recursive: bool, - options: ReadDirOptions<'_>, -) -> Result> { - let mut files_and_dirs: Vec = vec![]; - for entry in fs::read_dir(path)? { - let path = entry?.path(); - let path_as_string = path.display().to_string(); - - if let Ok(flag) = path.metadata().map(|m| m.is_dir()) { - let is_symlink = symlink_metadata(&path).map(|md| md.is_symlink())?; - files_and_dirs.push(DiskEntry { - path: path.clone(), - children: if flag { - Some( - if recursive - && (!is_symlink - || options.scope.map(|s| s.is_allowed(&path)).unwrap_or(true)) - { - read_dir_with_options(&path_as_string, true, options)? - } else { - vec![] - }, - ) - } else { - None - }, - name: path - .file_name() - .map(|name| name.to_string_lossy()) - .map(|name| name.to_string()), - }); - } - } - Result::Ok(files_and_dirs) +#[derive(Debug, Clone, Deserialize)] +pub struct RemoveOptions { + #[serde(flatten)] + base: BaseOptions, + recursive: Option, } #[tauri::command] -pub fn read_dir( - window: Window, +pub fn remove( + app: AppHandle, path: SafePathBuf, - options: Option, -) -> CommandResult> { - let (recursive, dir) = if let Some(options_value) = options { - (options_value.recursive, options_value.dir) + options: Option, +) -> CommandResult<()> { + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base.base_dir))?; + + let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| { + format!( + "failed to get metadata of path: {} with error: {e}", + resolved_path.display() + ) + })?; + + let file_type = metadata.file_type(); + + // taken from deno source code: https://github.com/denoland/deno/blob/429759fe8b4207240709c240a8344d12a1e39566/runtime/ops/fs.rs#L728 + let res = if file_type.is_file() { + std::fs::remove_file(&resolved_path) + } else if options.as_ref().and_then(|o| o.recursive).unwrap_or(false) { + std::fs::remove_dir_all(&resolved_path) + } else if file_type.is_symlink() { + #[cfg(unix)] + { + std::fs::remove_file(&resolved_path) + } + #[cfg(not(unix))] + { + use std::os::windows::fs::MetadataExt; + const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x00000010; + if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { + std::fs::remove_dir(&resolved_path) + } else { + std::fs::remove_file(&resolved_path) + } + } + } else if file_type.is_dir() { + std::fs::remove_dir(&resolved_path) } else { - (false, None) + // pipes, sockets, etc... + std::fs::remove_file(&resolved_path) }; - let resolved_path = resolve_path(&window, path, dir)?; - read_dir_with_options( - &resolved_path, - recursive, - ReadDirOptions { - scope: Some(window.fs_scope()), - }, - ) - .with_context(|| format!("path: {}", resolved_path.display())) + + res.map_err(|e| { + format!( + "failed to remove path: {} with error: {e}", + resolved_path.display() + ) + }) .map_err(Into::into) } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameOptions { + new_path_base_dir: Option, + old_path_base_dir: Option, +} + #[tauri::command] -pub fn copy_file( - window: Window, - source: SafePathBuf, - destination: SafePathBuf, - options: Option, +pub fn rename( + app: AppHandle, + old_path: SafePathBuf, + new_path: SafePathBuf, + options: Option, ) -> CommandResult<()> { - match options.and_then(|o| o.dir) { - Some(dir) => { - let src = resolve_path(&window, source, Some(dir))?; - let dest = resolve_path(&window, destination, Some(dir))?; - fs::copy(&src, &dest) - .with_context(|| format!("source: {}, dest: {}", src.display(), dest.display()))? - } - None => fs::copy(&source, &destination).with_context(|| { + let resolved_old_path = resolve_path( + &app, + old_path, + options.as_ref().and_then(|o| o.old_path_base_dir), + )?; + let resolved_new_path = resolve_path( + &app, + new_path, + options.as_ref().and_then(|o| o.new_path_base_dir), + )?; + std::fs::rename(&resolved_old_path, &resolved_new_path) + .map_err(|e| { format!( - "source: {}, dest: {}", - source.display(), - destination.display() + "failed to rename old path: {} to new path: {} with error: {e}", + resolved_old_path.display(), + resolved_new_path.display() ) - })?, - }; - Ok(()) + }) + .map_err(Into::into) +} + +#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] +#[repr(u16)] +pub enum SeekMode { + Start = 0, + Current = 1, + End = 2, +} + +#[tauri::command] +pub fn seek( + app: AppHandle, + rid: ResourceId, + offset: i64, + whence: SeekMode, +) -> CommandResult { + use std::io::{Seek, SeekFrom}; + let file = app.resources_table().get::(rid)?; + StdFileResource::with_lock(&file, |mut file| { + file.seek(match whence { + SeekMode::Start => SeekFrom::Start(offset as u64), + SeekMode::Current => SeekFrom::Current(offset), + SeekMode::End => SeekFrom::End(offset), + }) + }) + .map_err(|e| format!("failed to seek file with error: {e}")) + .map_err(Into::into) +} + +#[tauri::command] +pub fn stat( + app: AppHandle, + path: SafePathBuf, + options: Option, +) -> CommandResult { + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base_dir))?; + 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)) +} + +#[tauri::command] +pub fn lstat( + app: AppHandle, + path: SafePathBuf, + options: Option, +) -> CommandResult { + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base_dir))?; + let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| { + format!( + "failed to get metadata of path: {} with error: {e}", + resolved_path.display() + ) + })?; + Ok(get_stat(metadata)) +} + +#[tauri::command] +pub fn fstat(app: AppHandle, rid: ResourceId) -> CommandResult { + let file = app.resources_table().get::(rid)?; + let metadata = StdFileResource::with_lock(&file, |file| file.metadata()) + .map_err(|e| format!("failed to get metadata of file with error: {e}"))?; + Ok(get_stat(metadata)) } #[tauri::command] -pub fn create_dir( - window: Window, +pub fn truncate( + app: AppHandle, path: SafePathBuf, - options: Option, + len: Option, + options: Option, ) -> CommandResult<()> { - let (recursive, dir) = if let Some(options_value) = options { - (options_value.recursive, options_value.dir) - } else { - (false, None) - }; - let resolved_path = resolve_path(&window, path, dir)?; - if recursive { - fs::create_dir_all(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display()))?; - } else { - fs::create_dir(&resolved_path) - .with_context(|| format!("path: {} (non recursive)", resolved_path.display()))?; - } + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base_dir))?; + let f = std::fs::OpenOptions::new() + .write(true) + .open(&resolved_path) + .map_err(|e| { + format!( + "failed to open file at path: {} with error: {e}", + resolved_path.display() + ) + })?; + f.set_len(len.unwrap_or(0)) + .map_err(|e| { + format!( + "failed to truncate file at path: {} with error: {e}", + resolved_path.display() + ) + }) + .map_err(Into::into) +} - Ok(()) +#[tauri::command] +pub fn ftruncate( + app: AppHandle, + rid: ResourceId, + len: Option, +) -> CommandResult<()> { + let file = app.resources_table().get::(rid)?; + StdFileResource::with_lock(&file, |file| file.set_len(len.unwrap_or(0))) + .map_err(|e| format!("failed to truncate file with error: {e}")) + .map_err(Into::into) } #[tauri::command] -pub fn remove_dir( - window: Window, +pub fn write( + app: AppHandle, + rid: ResourceId, + data: Vec, +) -> CommandResult { + let file = app.resources_table().get::(rid)?; + StdFileResource::with_lock(&file, |mut file| file.write(&data)) + .map_err(|e| format!("failed to write bytes to file with error: {e}")) + .map_err(Into::into) +} + +#[derive(Debug, Clone, Deserialize)] +pub struct WriteFileOptions { + #[serde(flatten)] + base: BaseOptions, + append: Option, + create: Option, + #[allow(unused)] + mode: Option, +} + +fn write_file_inner( + app: AppHandle, path: SafePathBuf, - options: Option, + data: &[u8], + options: Option, ) -> CommandResult<()> { - let (recursive, dir) = if let Some(options_value) = options { - (options_value.recursive, options_value.dir) - } else { - (false, None) - }; - let resolved_path = resolve_path(&window, path, dir)?; - if recursive { - fs::remove_dir_all(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display()))?; - } else { - fs::remove_dir(&resolved_path) - .with_context(|| format!("path: {} (non recursive)", resolved_path.display()))?; + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base.base_dir))?; + + let mut opts = std::fs::OpenOptions::new(); + opts.append(options.as_ref().map(|o| o.append.unwrap_or(false)).unwrap()); + opts.create(options.as_ref().map(|o| o.create.unwrap_or(true)).unwrap()); + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + if let Some(Some(mode)) = options.map(|o| o.mode) { + opts.mode(mode & 0o777); + } } - Ok(()) + let mut file = opts.write(true).open(&resolved_path).map_err(|e| { + format!( + "failed to open file at path: {} with error: {e}", + resolved_path.display() + ) + })?; + + file.write_all(data) + .map_err(|e| { + format!( + "failed to write bytes to file at path: {} with error: {e}", + resolved_path.display() + ) + }) + .map_err(Into::into) } #[tauri::command] -pub fn remove_file( - window: Window, +pub fn write_file( + app: AppHandle, path: SafePathBuf, - options: Option, + data: Vec, + options: Option, ) -> CommandResult<()> { - let resolved_path = resolve_path(&window, path, options.and_then(|o| o.dir))?; - fs::remove_file(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display()))?; - Ok(()) + write_file_inner(app, path, &data, options) } #[tauri::command] -pub fn rename_file( - window: Window, - old_path: SafePathBuf, - new_path: SafePathBuf, - options: Option, +pub fn write_text_file( + app: AppHandle, + path: SafePathBuf, + data: String, + options: Option, ) -> CommandResult<()> { - match options.and_then(|o| o.dir) { - Some(dir) => { - let old = resolve_path(&window, old_path, Some(dir))?; - let new = resolve_path(&window, new_path, Some(dir))?; - fs::rename(&old, &new) - .with_context(|| format!("old: {}, new: {}", old.display(), new.display()))? - } - None => fs::rename(&old_path, &new_path) - .with_context(|| format!("old: {}, new: {}", old_path.display(), new_path.display()))?, - } - Ok(()) + write_file_inner(app, path, data.as_bytes(), options) } #[tauri::command] pub fn exists( - window: Window, + app: AppHandle, path: SafePathBuf, - options: Option, + options: Option, ) -> CommandResult { - let resolved_path = resolve_path(&window, path, options.and_then(|o| o.dir))?; + let resolved_path = resolve_path(&app, path, options.as_ref().and_then(|o| o.base_dir))?; Ok(resolved_path.exists()) } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Permissions { - readonly: bool, - #[cfg(unix)] - mode: u32, +pub fn resolve_path( + app: &AppHandle, + path: SafePathBuf, + base_dir: Option, +) -> CommandResult { + let path = file_url_to_safe_pathbuf(path)?; + let path = if let Some(base_dir) = base_dir { + app.path() + .resolve(&path, base_dir) + .map_err(Error::CannotResolvePath)? + } else { + path.as_ref().to_path_buf() + }; + if app.fs_scope().is_allowed(&path) { + Ok(path) + } else { + Err(CommandError::Plugin(Error::PathForbidden(path))) + } } -#[cfg(unix)] -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UnixMetadata { - dev: u64, - ino: u64, - mode: u32, - nlink: u64, - uid: u32, - gid: u32, - rdev: u64, - blksize: u64, - blocks: u64, -} - -#[derive(Serialize)] +#[inline] +fn file_url_to_safe_pathbuf(path: SafePathBuf) -> CommandResult { + 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); + +impl StdFileResource { + fn new(file: File) -> Self { + Self(Mutex::new(file)) + } + + fn with_lock R>(&self, mut f: F) -> R { + let file = self.0.lock().unwrap(); + f(&file) + } +} + +impl Resource for StdFileResource {} + +struct StdLinesResource(Mutex>>); + +impl StdLinesResource { + fn new(lines: Lines>) -> Self { + Self(Mutex::new(lines)) + } + + fn with_lock>) -> R>(&self, mut f: F) -> R { + let mut lines = self.0.lock().unwrap(); + f(&mut lines) + } +} + +impl Resource for StdLinesResource {} + +// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913 +#[inline] +fn to_msec(maybe_time: std::result::Result) -> Option { + match maybe_time { + Ok(time) => { + let msec = time + .duration_since(UNIX_EPOCH) + .map(|t| t.as_millis() as u64) + .unwrap_or_else(|err| err.duration().as_millis() as u64); + Some(msec) + } + Err(_) => None, + } +} + +// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L926 +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct Metadata { - accessed_at_ms: u64, - created_at_ms: u64, - modified_at_ms: u64, - is_dir: bool, +pub struct FileInfo { is_file: bool, + is_directory: bool, is_symlink: bool, size: u64, - permissions: Permissions, - #[cfg(unix)] - #[serde(flatten)] - unix: UnixMetadata, - #[cfg(windows)] - file_attributes: u32, + // In milliseconds, like JavaScript. Available on both Unix or Windows. + mtime: Option, + atime: Option, + birthtime: Option, + readonly: bool, + // Following are only valid under Windows. + file_attribues: Option, + // Following are only valid under Unix. + dev: Option, + ino: Option, + mode: Option, + nlink: Option, + uid: Option, + gid: Option, + rdev: Option, + blksize: Option, + blocks: Option, } -fn system_time_to_ms(time: std::io::Result) -> u64 { - time.map(|t| { - let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap(); - duration_since_epoch.as_millis() as u64 - }) - .unwrap_or_default() -} +// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L950 +#[inline(always)] +fn get_stat(metadata: std::fs::Metadata) -> FileInfo { + // Unix stat member (number types only). 0 if not on unix. + macro_rules! usm { + ($member:ident) => {{ + #[cfg(unix)] + { + Some(metadata.$member()) + } + #[cfg(not(unix))] + { + None + } + }}; + } -#[tauri::command] -pub async fn metadata(path: PathBuf) -> Result { - let metadata = std::fs::metadata(path)?; - let file_type = metadata.file_type(); - let permissions = metadata.permissions(); - Ok(Metadata { - accessed_at_ms: system_time_to_ms(metadata.accessed()), - created_at_ms: system_time_to_ms(metadata.created()), - modified_at_ms: system_time_to_ms(metadata.modified()), - is_dir: file_type.is_dir(), - is_file: file_type.is_file(), - is_symlink: file_type.is_symlink(), + #[cfg(unix)] + use std::os::unix::fs::MetadataExt; + #[cfg(windows)] + use std::os::windows::fs::MetadataExt; + FileInfo { + is_file: metadata.is_file(), + is_directory: metadata.is_dir(), + is_symlink: metadata.file_type().is_symlink(), size: metadata.len(), - permissions: Permissions { - readonly: permissions.readonly(), - #[cfg(unix)] - mode: permissions.mode(), - }, - #[cfg(unix)] - unix: UnixMetadata { - dev: metadata.dev(), - ino: metadata.ino(), - mode: metadata.mode(), - nlink: metadata.nlink(), - uid: metadata.uid(), - gid: metadata.gid(), - rdev: metadata.rdev(), - blksize: metadata.blksize(), - blocks: metadata.blocks(), - }, + // In milliseconds, like JavaScript. Available on both Unix or Windows. + mtime: to_msec(metadata.modified()), + atime: to_msec(metadata.accessed()), + birthtime: to_msec(metadata.created()), + readonly: metadata.permissions().readonly(), + // Following are only valid under Windows. #[cfg(windows)] - file_attributes: metadata.file_attributes(), - }) + file_attribues: Some(metadata.file_attributes()), + #[cfg(not(windows))] + file_attribues: None, + // Following are only valid under Unix. + dev: usm!(dev), + ino: usm!(ino), + mode: usm!(mode), + nlink: usm!(nlink), + uid: usm!(uid), + gid: usm!(gid), + rdev: usm!(rdev), + blksize: usm!(blksize), + blocks: usm!(blocks), + } } diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index d2004168..fda482d0 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -49,17 +49,29 @@ pub fn init() -> TauriPlugin> { PluginBuilder::>::new("fs") .js_init_script(include_str!("api-iife.js").to_string()) .invoke_handler(tauri::generate_handler![ + commands::create, + commands::open, + commands::copy_file, + commands::close, + commands::mkdir, + commands::read_dir, + commands::read, commands::read_file, commands::read_text_file, + commands::read_text_file_lines, + commands::read_text_file_lines_next, + commands::remove, + commands::rename, + commands::seek, + commands::stat, + commands::lstat, + commands::fstat, + commands::truncate, + commands::ftruncate, + commands::write, commands::write_file, - commands::read_dir, - commands::copy_file, - commands::create_dir, - commands::remove_dir, - commands::remove_file, - commands::rename_file, + commands::write_text_file, commands::exists, - commands::metadata, #[cfg(feature = "watch")] watcher::watch, #[cfg(feature = "watch")] @@ -75,9 +87,6 @@ pub fn init() -> TauriPlugin> { .unwrap_or(&default_scope), )?); - #[cfg(feature = "watch")] - app.manage(watcher::WatcherCollection::default()); - Ok(()) }) .on_event(|app, event| { diff --git a/plugins/fs/src/watcher.rs b/plugins/fs/src/watcher.rs index ee7adbbe..05683586 100644 --- a/plugins/fs/src/watcher.rs +++ b/plugins/fs/src/watcher.rs @@ -5,12 +5,13 @@ use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer}; use serde::Deserialize; -use tauri::{command, ipc::Channel, State}; - -use crate::Result; +use tauri::{ + ipc::Channel, + path::{BaseDirectory, SafePathBuf}, + AppHandle, Manager, Resource, ResourceId, Runtime, +}; use std::{ - collections::HashMap, path::PathBuf, sync::{ mpsc::{channel, Receiver}, @@ -20,10 +21,26 @@ use std::{ time::Duration, }; -type Id = u32; +use crate::commands::{resolve_path, CommandResult}; + +struct InnerWatcher { + pub kind: WatcherKind, + paths: Vec, +} + +pub struct WatcherResource(Mutex); +impl WatcherResource { + fn new(kind: WatcherKind, paths: Vec) -> Self { + Self(Mutex::new(InnerWatcher { kind, paths })) + } + + fn with_lock R>(&self, mut f: F) -> R { + let mut watcher = self.0.lock().unwrap(); + f(&mut watcher) + } +} -#[derive(Default)] -pub struct WatcherCollection(Mutex)>>); +impl Resource for WatcherResource {} enum WatcherKind { Debouncer(Debouncer), @@ -55,63 +72,76 @@ fn watch_debounced(on_event: Channel, rx: Receiver) { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct WatchOptions { - delay_ms: Option, + dir: Option, recursive: bool, + delay_ms: Option, } -#[command] -pub async fn watch( - watchers: State<'_, WatcherCollection>, - id: Id, - paths: Vec, +#[tauri::command] +pub async fn watch( + app: AppHandle, + paths: Vec, options: WatchOptions, on_event: Channel, -) -> Result<()> { +) -> CommandResult { + let mut resolved_paths = Vec::with_capacity(paths.capacity()); + for path in paths { + resolved_paths.push(resolve_path(&app, path, options.dir)?); + } + let mode = if options.recursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive }; - let watcher = if let Some(delay) = options.delay_ms { + let kind = if let Some(delay) = options.delay_ms { let (tx, rx) = channel(); let mut debouncer = new_debouncer(Duration::from_millis(delay), tx)?; let watcher = debouncer.watcher(); - for path in &paths { - watcher.watch(path, mode)?; + for path in &resolved_paths { + watcher.watch(path.as_ref(), mode)?; } watch_debounced(on_event, rx); WatcherKind::Debouncer(debouncer) } else { let (tx, rx) = channel(); let mut watcher = RecommendedWatcher::new(tx, Config::default())?; - for path in &paths { - watcher.watch(path, mode)?; + for path in &resolved_paths { + watcher.watch(path.as_ref(), mode)?; } watch_raw(on_event, rx); WatcherKind::Watcher(watcher) }; - watchers.0.lock().unwrap().insert(id, (watcher, paths)); + let rid = app + .resources_table() + .add(WatcherResource::new(kind, resolved_paths)); - Ok(()) + Ok(rid) } -#[command] -pub async fn unwatch(watchers: State<'_, WatcherCollection>, id: Id) -> Result<()> { - if let Some((watcher, paths)) = watchers.0.lock().unwrap().remove(&id) { - match watcher { - WatcherKind::Debouncer(mut debouncer) => { - for path in paths { - debouncer.watcher().unwatch(&path)? +#[tauri::command] +pub async fn unwatch(app: AppHandle, rid: ResourceId) -> CommandResult<()> { + let watcher = app.resources_table().take::(rid)?; + WatcherResource::with_lock(&watcher, |watcher| { + match &mut watcher.kind { + WatcherKind::Debouncer(ref mut debouncer) => { + for path in &watcher.paths { + debouncer.watcher().unwatch(path.as_ref()).map_err(|e| { + format!("failed to unwatch path: {} with error: {e}", path.display()) + })? } } - WatcherKind::Watcher(mut watcher) => { - for path in paths { - watcher.unwatch(&path)? + WatcherKind::Watcher(ref mut w) => { + for path in &watcher.paths { + w.unwatch(path.as_ref()).map_err(|e| { + format!("failed to unwatch path: {} with error: {e}", path.display()) + })? } } - }; - } - Ok(()) + } + + Ok(()) + }) } diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml index 1bfaea7a..1ca16a47 100644 --- a/plugins/http/Cargo.toml +++ b/plugins/http/Cargo.toml @@ -20,7 +20,7 @@ tauri-plugin-fs = { path = "../fs", version = "2.0.0-alpha.5" } glob = "0.3" http = "0.2" reqwest = { version = "0.11", default-features = false } -url = "2.4" +url = { workspace = true } data-url = "0.3" [features] diff --git a/plugins/single-instance/src/platform_impl/windows.rs b/plugins/single-instance/src/platform_impl/windows.rs index 31c4f229..9501b19f 100644 --- a/plugins/single-instance/src/platform_impl/windows.rs +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -119,7 +119,7 @@ unsafe extern "system" fn single_instance_window_proc( let data = CStr::from_ptr((*cds_ptr).lpData as _).to_string_lossy(); let mut s = data.split('|'); let cwd = s.next().unwrap(); - let args = s.into_iter().map(|s| s.to_string()).collect(); + let args = s.map(|s| s.to_string()).collect(); callback(app_handle, args, cwd.to_string()); } 1 diff --git a/plugins/updater/Cargo.toml b/plugins/updater/Cargo.toml index 9a08af37..461a69b9 100644 --- a/plugins/updater/Cargo.toml +++ b/plugins/updater/Cargo.toml @@ -18,7 +18,7 @@ serde_json = { workspace = true } thiserror = { workspace = true } tokio = "1" reqwest = { version = "0.11", default-features = false, features = [ "json", "stream" ] } -url = "2" +url = { workspace = true } http = "0.2" dirs-next = "2" minisign-verify = "0.2"