diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dfb7251..7a23a08 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -213,6 +224,40 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "base64" version = "0.21.7" @@ -225,6 +270,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -264,6 +315,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -401,6 +470,15 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.65" @@ -408,6 +486,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -444,6 +524,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -456,6 +553,31 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "combine" version = "4.6.7" @@ -475,6 +597,38 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "cookie" version = "0.18.1" @@ -534,6 +688,24 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -558,6 +730,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -568,6 +746,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.36.0" @@ -601,12 +788,31 @@ dependencies = [ "dtor", ] +[[package]] +name = "ctor" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb22e947478ccf9dc44d8922042c677a63fbb88f2cb468521d1145816e5087cb" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + [[package]] name = "ctor-proc-macro" version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "darling" version = "0.21.3" @@ -653,6 +859,48 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -690,8 +938,22 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -761,6 +1023,15 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dom_query" version = "0.27.0" @@ -1011,6 +1282,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1020,6 +1297,21 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -1027,6 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1094,6 +1387,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1220,8 +1514,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1243,8 +1539,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -1332,6 +1631,18 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1401,6 +1712,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.17.1" @@ -1431,6 +1748,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "html5ever" version = "0.38.0" @@ -1486,6 +1821,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -1506,6 +1850,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1724,6 +2083,16 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1778,6 +2147,50 @@ dependencies = [ "system-deps", ] +[[package]] +name = "jiff" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" +dependencies = [ + "defmt", + "jiff-static", + "jiff-tzdb-platform", + "js-sys", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-static" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -1794,6 +2207,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -1822,6 +2265,16 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.103" @@ -1886,6 +2339,15 @@ dependencies = [ "libc", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -1935,6 +2397,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.17" @@ -1944,6 +2412,18 @@ dependencies = [ "libc", ] +[[package]] +name = "link-section" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e333fe507b738576d6da5bb3f1a7d7a1c80307ed9ef31624c057d844c19c93e9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7b0a3383c2a1002d11349c92c85a666a5fb679e96c79d782cf0dbe557fd6ee" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1971,6 +2451,12 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "markup5ever" version = "0.38.0" @@ -1982,6 +2468,25 @@ dependencies = [ "web_atoms", ] +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "mea" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2640d335e7273dacdcf51044026139b2e269c3bb0dfc3f8cb3496b85e3f6a42c" +dependencies = [ + "slab", +] + [[package]] name = "memchr" version = "2.8.2" @@ -2103,12 +2608,47 @@ dependencies = [ "bitflags 2.13.0", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2116,6 +2656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2353,12 +2894,152 @@ dependencies = [ "libc", ] +[[package]] +name = "opendal" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c9c85ce253ff87225e7669979d877a20c98a06604ec9d6dd5f4473e08f1ae1" +dependencies = [ + "ctor 1.0.8", + "opendal-core", + "opendal-layer-concurrent-limit", + "opendal-layer-logging", + "opendal-layer-retry", + "opendal-layer-timeout", + "opendal-service-s3", + "opendal-service-webdav", +] + +[[package]] +name = "opendal-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f8607c90e2c963a91467f50fb49fbc7fb3d573f88cea219ca59ccd3740b309" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytes", + "futures", + "http", + "http-body", + "jiff", + "log", + "md-5", + "mea", + "percent-encoding", + "quick-xml 0.39.4", + "reqsign-core", + "reqwest", + "serde", + "serde_json", + "tokio", + "url", + "uuid", + "web-time", +] + +[[package]] +name = "opendal-layer-concurrent-limit" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6f81ba6960e3fae1882f253b114b21d7e444e1534f209c7737a79f6243eb6f" +dependencies = [ + "futures", + "http", + "mea", + "opendal-core", +] + +[[package]] +name = "opendal-layer-logging" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ada45c6d81d1aa4c9305d0c7d4bc317c59c85866a0908a2d75a7a978aa5ee2" +dependencies = [ + "log", + "opendal-core", +] + +[[package]] +name = "opendal-layer-retry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2a25a718afb81fad81cb9a0580a1cb989221fa2317f888c6a37f8dad408eb7" +dependencies = [ + "backon", + "log", + "opendal-core", +] + +[[package]] +name = "opendal-layer-timeout" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91f731724c213af81e9d03517859c8fc47b4578e64ad61ae4f099f10fe36e3" +dependencies = [ + "opendal-core", + "tokio", +] + +[[package]] +name = "opendal-service-s3" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313d46c9f5ae70bca26b7c3e3fbb9b639292625f28af73aa016f47e788af9deb" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc32c", + "http", + "log", + "md-5", + "opendal-core", + "quick-xml 0.39.4", + "reqsign-aws-v4", + "reqsign-core", + "reqsign-file-read-tokio", + "serde", + "url", +] + +[[package]] +name = "opendal-service-webdav" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9edadbbf8311e4d382400a5c6021bbfcc850f472a60995897bdc5cbf2d1cabd" +dependencies = [ + "anyhow", + "bytes", + "http", + "log", + "mea", + "opendal-core", + "quick-xml 0.39.4", + "serde", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -2423,6 +3104,25 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2499,6 +3199,44 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2 0.10.9", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core 0.6.4", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2513,7 +3251,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -2558,6 +3296,21 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2573,6 +3326,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -2650,6 +3412,83 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.46" @@ -2671,6 +3510,61 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2746,6 +3640,64 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqsign-aws-v4" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75624bd8a466e37ddc0a7b6c33ac859a85347c153a916e1dd9d0b68338f74a" +dependencies = [ + "anyhow", + "bytes", + "form_urlencoded", + "hex", + "http", + "log", + "percent-encoding", + "quick-xml 0.40.1", + "reqsign-core", + "rust-ini", + "serde", + "serde_json", + "serde_urlencoded", + "sha1", +] + +[[package]] +name = "reqsign-core" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fa5cb48808693614d1701fcd3db0b30fa292e0f18e122ae068b6d32eaeed3f" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures", + "hex", + "hmac 0.13.0", + "http", + "jiff", + "log", + "percent-encoding", + "rsa", + "serde", + "serde_json", + "sha1", + "sha2 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "reqsign-file-read-tokio" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a4b6f3a3fd29ffcc99a90aec585a65217783badfd73acddf847b63ae683bda9" +dependencies = [ + "anyhow", + "reqsign-core", + "tokio", +] + [[package]] name = "reqwest" version = "0.13.4" @@ -2760,15 +3712,21 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -2804,6 +3762,51 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2832,12 +3835,102 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2847,6 +3940,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -2904,6 +4006,40 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2 0.10.9", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.36.1" @@ -3028,6 +4164,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.17.0" @@ -3090,6 +4238,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3097,8 +4256,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3117,12 +4287,38 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.3" @@ -3199,6 +4395,22 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3235,6 +4447,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3318,7 +4536,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -3375,7 +4593,7 @@ dependencies = [ "heck 0.5.0", "http", "http-range", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3447,7 +4665,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 2.0.118", "tauri-utils", "thiserror 2.0.18", @@ -3561,7 +4779,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -3584,7 +4802,7 @@ checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -3611,7 +4829,7 @@ dependencies = [ "anyhow", "brotli", "cargo_metadata", - "ctor", + "ctor 0.8.0", "dom_query", "dunce", "glob", @@ -3745,6 +4963,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -3755,6 +4982,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.3" @@ -3766,9 +5008,31 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4082,6 +5346,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4275,6 +5545,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.5" @@ -4331,6 +5611,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4561,6 +5850,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4856,7 +6154,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -4868,7 +6166,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.18", @@ -4932,6 +6230,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "notify", + "opendal", "regex", "serde", "serde_json", @@ -4940,6 +6239,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-opener", "time", + "tokio", ] [[package]] @@ -5003,6 +6303,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -5024,6 +6344,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bc483c3..a7d3d37 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,4 +27,5 @@ serde_json = "1" time = ">=0.3.0, <0.3.47" notify = "8.2.0" regex = "1" - +opendal = { version = "0.57.0", features = ["services-s3", "services-webdav"] } +tokio = { version = "1", features = ["rt-multi-thread", "time"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 88ec54e..d5967a3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,14 +1,26 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ use notify::{RecursiveMode, Watcher}; +use opendal::services::{S3, Webdav}; +use opendal::{EntryMode, ErrorKind, Operator}; +use std::collections::{BTreeMap, BTreeSet}; +use std::future::{Future, IntoFuture}; use std::fs; -use std::path::PathBuf; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; use std::sync::Mutex; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder}; use tauri::{Emitter, Manager}; use tauri_plugin_dialog::DialogExt; use base64::Engine; use regex::Regex; +const SYNC_CHECK_TIMEOUT: Duration = Duration::from_secs(15); +const SYNC_TEST_STEP_TIMEOUT: Duration = Duration::from_secs(15); +const SYNC_REMOTE_LIST_TIMEOUT: Duration = Duration::from_secs(120); +const SYNC_REMOTE_FILE_TIMEOUT: Duration = Duration::from_secs(180); +const SYNC_TEST_PAYLOAD: &[u8] = b"yurou sync connection probe\n"; + #[derive(Debug, Clone, serde::Serialize)] struct DirEntry { name: String, @@ -455,6 +467,845 @@ fn walk_dir( Ok(()) } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SyncConfig { + provider: SyncProvider, + webdav: WebdavSyncConfig, + s3: S3SyncConfig, + auto_sync_on_startup: bool, + auto_sync_after_save: bool, + last_sync_at: Option, +} + +impl Default for SyncConfig { + fn default() -> Self { + Self { + provider: SyncProvider::None, + webdav: WebdavSyncConfig::default(), + s3: S3SyncConfig::default(), + auto_sync_on_startup: false, + auto_sync_after_save: false, + last_sync_at: None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +enum SyncProvider { + None, + Webdav, + S3, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WebdavSyncConfig { + endpoint: String, + username: String, + password: String, + remote_dir: String, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct S3SyncConfig { + endpoint: String, + region: String, + bucket: String, + access_key_id: String, + secret_access_key: String, + remote_prefix: String, + path_style: bool, + use_https: bool, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SyncHistory { + provider_key: String, + files: BTreeMap, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SyncHistoryEntry { + local_modified_ms: Option, + remote_modified_ms: Option, + size: u64, +} + +#[derive(Debug, Clone)] +struct SyncFileInfo { + modified_ms: Option, + size: u64, +} + +#[derive(Debug, Clone, Copy, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct SyncStats { + uploaded: usize, + downloaded: usize, + deleted_local: usize, + deleted_remote: usize, + skipped: usize, +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct SyncResult { + message: String, + last_sync_at: String, + stats: SyncStats, +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct SyncConnectionTestResult { + message: String, + details: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SyncAction { + Upload, + Download, + DeleteLocal, + DeleteRemote, + Skip, +} + +fn tauri_app_config_dir(app: &tauri::AppHandle) -> Result { + app.path() + .app_config_dir() + .map_err(|e| format!("无法获取配置目录: {}", e)) +} + +fn sync_config_path(app: &tauri::AppHandle) -> Result { + Ok(tauri_app_config_dir(app)?.join("sync-config.json")) +} + +fn sync_history_path(app: &tauri::AppHandle, provider_key: &str) -> Result { + Ok(tauri_app_config_dir(app)?.join(format!("sync-history-{}.json", stable_hash(provider_key)))) +} + +fn stable_hash(value: &str) -> u64 { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + +fn load_sync_config_from_disk(app: &tauri::AppHandle) -> Result { + let path = sync_config_path(app)?; + if !path.exists() { + return Ok(SyncConfig::default()); + } + + let raw = fs::read_to_string(&path).map_err(|e| format!("无法读取同步配置: {}", e))?; + serde_json::from_str(&raw).map_err(|e| format!("同步配置格式无效: {}", e)) +} + +fn save_sync_config_to_disk(app: &tauri::AppHandle, config: &SyncConfig) -> Result<(), String> { + let path = sync_config_path(app)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|e| format!("无法创建配置目录: {}", e))?; + } + + let raw = serde_json::to_string_pretty(config).map_err(|e| format!("无法序列化同步配置: {}", e))?; + fs::write(&path, raw).map_err(|e| format!("无法保存同步配置: {}", e)) +} + +fn load_sync_history(app: &tauri::AppHandle, provider_key: &str) -> Result { + let path = sync_history_path(app, provider_key)?; + if !path.exists() { + return Ok(SyncHistory { + provider_key: provider_key.to_string(), + files: BTreeMap::new(), + }); + } + + let raw = fs::read_to_string(&path).map_err(|e| format!("无法读取同步历史: {}", e))?; + let mut history: SyncHistory = + serde_json::from_str(&raw).map_err(|e| format!("同步历史格式无效: {}", e))?; + history.provider_key = provider_key.to_string(); + Ok(history) +} + +fn save_sync_history(app: &tauri::AppHandle, history: &SyncHistory) -> Result<(), String> { + let path = sync_history_path(app, &history.provider_key)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|e| format!("无法创建同步历史目录: {}", e))?; + } + + let raw = serde_json::to_string_pretty(history).map_err(|e| format!("无法序列化同步历史: {}", e))?; + fs::write(&path, raw).map_err(|e| format!("无法保存同步历史: {}", e)) +} + +fn normalize_remote_root(path: &str) -> String { + let trimmed = path.trim().replace('\\', "/"); + let trimmed = trimmed.trim_matches('/'); + if trimmed.is_empty() { + "/".to_string() + } else { + format!("/{}", trimmed) + } +} + +fn normalize_s3_prefix(prefix: &str) -> String { + prefix + .trim() + .replace('\\', "/") + .trim_matches('/') + .to_string() +} + +fn normalize_remote_file_path(path: &str) -> String { + path.trim_start_matches('/').replace('\\', "/") +} + +fn normalize_relative_path(path: &Path) -> String { + path.components() + .filter_map(|component| match component { + std::path::Component::Normal(part) => Some(part.to_string_lossy().to_string()), + _ => None, + }) + .collect::>() + .join("/") +} + +fn provider_key(config: &SyncConfig) -> Result { + match config.provider { + SyncProvider::None => Err("请先选择同步方式".to_string()), + SyncProvider::Webdav => { + let endpoint = config.webdav.endpoint.trim(); + if endpoint.is_empty() { + return Err("请填写 WebDAV 服务器地址".to_string()); + } + Ok(format!( + "webdav:{}:{}", + endpoint.trim_end_matches('/'), + normalize_remote_root(&config.webdav.remote_dir) + )) + } + SyncProvider::S3 => { + let bucket = config.s3.bucket.trim(); + if bucket.is_empty() { + return Err("请填写 S3 Bucket".to_string()); + } + Ok(format!( + "s3:{}:{}:{}", + config.s3.endpoint.trim().trim_end_matches('/'), + bucket, + normalize_s3_prefix(&config.s3.remote_prefix) + )) + } + } +} + +fn build_sync_operator(config: &SyncConfig) -> Result { + match config.provider { + SyncProvider::None => Err("请先选择同步方式".to_string()), + SyncProvider::Webdav => { + let endpoint = config.webdav.endpoint.trim(); + if endpoint.is_empty() { + return Err("请填写 WebDAV 服务器地址".to_string()); + } + + let mut builder = Webdav::default() + .endpoint(endpoint) + .root(&normalize_remote_root(&config.webdav.remote_dir)); + + if !config.webdav.username.trim().is_empty() { + builder = builder.username(config.webdav.username.trim()); + } + if !config.webdav.password.is_empty() { + builder = builder.password(&config.webdav.password); + } + + Operator::new(builder) + .map(|builder| builder.finish()) + .map_err(|e| format!("无法初始化 WebDAV 同步: {}", e)) + } + SyncProvider::S3 => { + let bucket = config.s3.bucket.trim(); + if bucket.is_empty() { + return Err("请填写 S3 Bucket".to_string()); + } + + let mut endpoint = config.s3.endpoint.trim().trim_end_matches('/').to_string(); + if !endpoint.is_empty() && !endpoint.contains("://") { + endpoint = if config.s3.use_https { + format!("https://{}", endpoint) + } else { + format!("http://{}", endpoint) + }; + } + + let mut builder = S3::default() + .bucket(bucket) + .root(&normalize_s3_prefix(&config.s3.remote_prefix)) + .disable_config_load(); + + if !endpoint.is_empty() { + builder = builder.endpoint(&endpoint); + } + if !config.s3.region.trim().is_empty() { + builder = builder.region(config.s3.region.trim()); + } + if !config.s3.access_key_id.trim().is_empty() { + builder = builder.access_key_id(config.s3.access_key_id.trim()); + } + if !config.s3.secret_access_key.is_empty() { + builder = builder.secret_access_key(&config.s3.secret_access_key); + } + if !config.s3.path_style { + builder = builder.enable_virtual_host_style(); + } + + Operator::new(builder) + .map(|builder| builder.finish()) + .map_err(|e| format!("无法初始化 S3 同步: {}", e)) + } + } +} + +async fn opendal_timeout(step: &str, timeout: Duration, future: F) -> Result +where + F: Future>, +{ + match tokio::time::timeout(timeout, future).await { + Ok(Ok(value)) => Ok(value), + Ok(Err(error)) => Err(format!("{}失败: {}", step, describe_opendal_error(&error))), + Err(_) => Err(format!("{}超时({} 秒)", step, timeout.as_secs())), + } +} + +async fn test_step( + details: &mut Vec, + step: &str, + success: &str, + future: F, +) -> Result +where + F: Future>, +{ + let start = Instant::now(); + match tokio::time::timeout(SYNC_TEST_STEP_TIMEOUT, future).await { + Ok(Ok(value)) => { + details.push(format!( + "{}: {},耗时 {}ms", + step, + success, + start.elapsed().as_millis() + )); + Ok(value) + } + Ok(Err(error)) => Err(format_sync_test_failure( + step, + describe_opendal_error(&error), + details, + )), + Err(_) => Err(format_sync_test_failure( + step, + format!("请求超时({} 秒),请检查服务器地址、网络或远端目录响应速度", SYNC_TEST_STEP_TIMEOUT.as_secs()), + details, + )), + } +} + +fn describe_opendal_error(error: &opendal::Error) -> String { + let hint = match error.kind() { + ErrorKind::ConfigInvalid => "配置无效,请检查服务器地址、Bucket、Region 或远端路径", + ErrorKind::PermissionDenied => "权限被拒绝,通常表示账号/密码/Access Key 不正确,或该账号没有远端目录权限", + ErrorKind::NotFound => "远端路径不存在,请检查 WebDAV 远端目录、S3 Bucket 或前缀", + ErrorKind::Unsupported => "服务不支持当前操作,请确认 WebDAV/S3 兼容能力", + ErrorKind::RateLimited => "服务限流,请稍后重试", + ErrorKind::IsADirectory => "目标是目录,不是文件", + ErrorKind::NotADirectory => "目标不是目录", + ErrorKind::AlreadyExists => "目标已存在", + ErrorKind::ConditionNotMatch => "服务端条件校验未通过", + ErrorKind::RangeNotSatisfied => "读取范围无效", + ErrorKind::IsSameFile => "源和目标是同一个文件", + ErrorKind::Unexpected => "服务返回了非预期错误,请检查原始错误信息", + _ => "服务返回了未知错误", + }; + + format!("{};原始错误: {}", hint, error) +} + +fn format_sync_test_failure(step: &str, reason: String, details: &[String]) -> String { + let mut lines = vec![format!("连接测试失败,失败步骤:{}", step), reason]; + if !details.is_empty() { + lines.push("已通过步骤:".to_string()); + lines.extend(details.iter().map(|detail| format!("- {}", detail))); + } + lines.join("\n") +} + +fn sync_target_label(config: &SyncConfig) -> String { + match config.provider { + SyncProvider::None => "未选择同步方式".to_string(), + SyncProvider::Webdav => format!( + "WebDAV {}{}", + config.webdav.endpoint.trim(), + normalize_remote_root(&config.webdav.remote_dir) + ), + SyncProvider::S3 => format!( + "S3 bucket={} prefix={}", + config.s3.bucket.trim(), + normalize_s3_prefix(&config.s3.remote_prefix) + ), + } +} + +fn sync_probe_file_path() -> String { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis()) + .unwrap_or_default(); + format!("_yurou-sync-probe-{}-{}.tmp", std::process::id(), millis) +} + +fn remote_parent_dirs(relative: &str) -> Vec { + let parts: Vec<&str> = relative.split('/').filter(|part| !part.is_empty()).collect(); + if parts.len() <= 1 { + return Vec::new(); + } + + let mut dirs = Vec::new(); + let mut current = String::new(); + for part in parts.iter().take(parts.len() - 1) { + current.push_str(part); + current.push('/'); + dirs.push(current.clone()); + } + dirs +} + +async fn ensure_remote_parent_dirs(op: &Operator, relative: &str) -> Result<(), String> { + for dir in remote_parent_dirs(relative) { + match tokio::time::timeout(SYNC_REMOTE_FILE_TIMEOUT, op.create_dir(&dir)).await { + Ok(Ok(())) => {} + Ok(Err(error)) if error.kind() == ErrorKind::AlreadyExists => {} + Ok(Err(error)) => { + return Err(format!( + "创建远端目录 {}失败: {}", + dir, + describe_opendal_error(&error) + )); + } + Err(_) => { + return Err(format!( + "创建远端目录 {}超时({} 秒)", + dir, + SYNC_REMOTE_FILE_TIMEOUT.as_secs() + )); + } + } + } + Ok(()) +} + +async fn run_sync_connection_test(config: &SyncConfig) -> Result { + let mut details = Vec::new(); + let op = build_sync_operator(config)?; + details.push(format!("配置解析通过,目标:{}", sync_target_label(config))); + + test_step(&mut details, "服务检查", "服务器可达", op.check()).await?; + test_step( + &mut details, + "目录读取", + "账号/密码有效,远端目录可读取", + op.list_with("/").limit(1).into_future(), + ) + .await?; + + let probe_path = sync_probe_file_path(); + test_step( + &mut details, + "写入探针", + "远端目录可写", + op.write(&probe_path, SYNC_TEST_PAYLOAD.to_vec()), + ) + .await?; + + let read_result = test_step( + &mut details, + "读取探针", + "写入内容可读回", + op.read(&probe_path), + ) + .await; + let read_bytes = match read_result { + Ok(bytes) => bytes.to_vec(), + Err(error) => { + let _ = opendal_timeout("清理测试文件", SYNC_TEST_STEP_TIMEOUT, op.delete(&probe_path)).await; + return Err(error); + } + }; + + if read_bytes != SYNC_TEST_PAYLOAD { + let _ = opendal_timeout("清理测试文件", SYNC_TEST_STEP_TIMEOUT, op.delete(&probe_path)).await; + return Err(format_sync_test_failure( + "读取探针", + "读回内容与写入内容不一致,请检查 WebDAV/S3 服务是否有缓存或代理改写".to_string(), + &details, + )); + } + + test_step( + &mut details, + "删除探针", + "远端目录可删除", + op.delete(&probe_path), + ) + .await?; + + Ok(SyncConnectionTestResult { + message: "连接可用,账号/密码正确,远端目录具备读取、写入和删除权限".to_string(), + details, + }) +} + +fn system_time_ms(time: SystemTime) -> Option { + match time.duration_since(UNIX_EPOCH) { + Ok(duration) => Some(duration.as_millis() as i128), + Err(error) => Some(-(error.duration().as_millis() as i128)), + } +} + +fn now_string() -> String { + opendal::raw::Timestamp::now().to_string() +} + +fn remote_modified_ms(meta: &opendal::Metadata) -> Option { + meta.last_modified() + .and_then(|value| { + let system_time: SystemTime = value.into(); + system_time_ms(system_time) + }) +} + +fn scan_local_files(root: &Path) -> Result, String> { + if !root.is_dir() { + return Err("工作目录不存在或不是文件夹".to_string()); + } + + let mut files = BTreeMap::new(); + scan_local_files_inner(root, root, &mut files)?; + Ok(files) +} + +fn scan_local_files_inner( + root: &Path, + current: &Path, + files: &mut BTreeMap, +) -> Result<(), String> { + for entry in fs::read_dir(current).map_err(|e| format!("无法读取工作目录: {}", e))? { + let entry = entry.map_err(|e| format!("读取工作目录条目失败: {}", e))?; + let path = entry.path(); + let file_type = entry + .file_type() + .map_err(|e| format!("无法读取文件类型: {}", e))?; + + if file_type.is_dir() { + scan_local_files_inner(root, &path, files)?; + } else if file_type.is_file() { + let metadata = entry + .metadata() + .map_err(|e| format!("无法读取文件元数据: {}", e))?; + let relative = path + .strip_prefix(root) + .map(normalize_relative_path) + .map_err(|e| format!("无法计算相对路径: {}", e))?; + files.insert( + relative.clone(), + SyncFileInfo { + modified_ms: metadata.modified().ok().and_then(system_time_ms), + size: metadata.len(), + }, + ); + } + } + + Ok(()) +} + +async fn scan_remote_files(op: &Operator) -> Result, String> { + let entries = opendal_timeout( + "读取远端文件列表", + SYNC_REMOTE_LIST_TIMEOUT, + op.list_with("/").recursive(true).into_future(), + ) + .await?; + + let mut files = BTreeMap::new(); + for entry in entries { + let (path, listed_meta) = entry.into_parts(); + if path == "/" || path.ends_with('/') || listed_meta.mode() == EntryMode::DIR { + continue; + } + + let meta = match tokio::time::timeout(SYNC_REMOTE_FILE_TIMEOUT, op.stat(&path)).await { + Ok(Ok(meta)) => meta, + Ok(Err(error)) if error.kind() == ErrorKind::NotFound => continue, + Ok(Err(error)) => { + return Err(format!( + "读取远端文件元数据 {}失败: {}", + path, + describe_opendal_error(&error) + )); + } + Err(_) => { + return Err(format!( + "读取远端文件元数据 {}超时({} 秒)", + path, + SYNC_REMOTE_FILE_TIMEOUT.as_secs() + )); + } + }; + + if !meta.is_file() { + continue; + } + + let relative = normalize_remote_file_path(&path); + files.insert( + relative.clone(), + SyncFileInfo { + modified_ms: remote_modified_ms(&meta), + size: meta.content_length(), + }, + ); + } + + Ok(files) +} + +fn choose_sync_action( + local: Option<&SyncFileInfo>, + remote: Option<&SyncFileInfo>, + history: Option<&SyncHistoryEntry>, +) -> SyncAction { + match (local, remote) { + (Some(local), Some(remote)) => { + if let Some(history) = history { + if history.local_modified_ms == local.modified_ms + && history.remote_modified_ms == remote.modified_ms + && history.size == local.size + && history.size == remote.size + { + return SyncAction::Skip; + } + } + + if local.size == remote.size + && local.modified_ms.is_some() + && remote.modified_ms.is_some() + && local.modified_ms == remote.modified_ms + { + return SyncAction::Skip; + } + + match (local.modified_ms, remote.modified_ms) { + (Some(local_ms), Some(remote_ms)) => { + if local_ms >= remote_ms { + SyncAction::Upload + } else { + SyncAction::Download + } + } + (Some(_), None) => SyncAction::Upload, + (None, Some(_)) => SyncAction::Download, + (None, None) => { + if local.size == remote.size { + SyncAction::Skip + } else { + SyncAction::Upload + } + } + } + } + (Some(local), None) => { + if let Some(history) = history { + if history.local_modified_ms == local.modified_ms && history.size == local.size { + SyncAction::DeleteLocal + } else { + SyncAction::Upload + } + } else { + SyncAction::Upload + } + } + (None, Some(remote)) => { + if let Some(history) = history { + if history.remote_modified_ms == remote.modified_ms && history.size == remote.size { + SyncAction::DeleteRemote + } else { + SyncAction::Download + } + } else { + SyncAction::Download + } + } + (None, None) => SyncAction::Skip, + } +} + +fn local_path_for(root: &Path, relative: &str) -> PathBuf { + relative + .split('/') + .filter(|part| !part.is_empty()) + .fold(root.to_path_buf(), |path, part| path.join(part)) +} + +async fn sync_workspace( + app: &tauri::AppHandle, + workspace: &Path, + config: &SyncConfig, +) -> Result { + let op = build_sync_operator(config)?; + opendal_timeout("同步连接检查", SYNC_CHECK_TIMEOUT, op.check()).await?; + + let key = provider_key(config)?; + let local_files = scan_local_files(workspace)?; + let remote_files = scan_remote_files(&op).await?; + let mut history = load_sync_history(app, &key)?; + let mut stats = SyncStats::default(); + + let all_paths: BTreeSet = local_files + .keys() + .chain(remote_files.keys()) + .chain(history.files.keys()) + .cloned() + .collect(); + + for relative in all_paths { + let local = local_files.get(&relative); + let remote = remote_files.get(&relative); + let prior = history.files.get(&relative); + + match choose_sync_action(local, remote, prior) { + SyncAction::Upload => { + let local_path = local_path_for(workspace, &relative); + let bytes = fs::read(&local_path) + .map_err(|e| format!("无法读取本地文件 {}: {}", local_path.display(), e))?; + ensure_remote_parent_dirs(&op, &relative).await?; + opendal_timeout( + &format!("上传 {}", relative), + SYNC_REMOTE_FILE_TIMEOUT, + op.write(&relative, bytes), + ) + .await?; + stats.uploaded += 1; + } + SyncAction::Download => { + let local_path = local_path_for(workspace, &relative); + if let Some(parent) = local_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("无法创建本地目录 {}: {}", parent.display(), e))?; + } + let bytes = opendal_timeout( + &format!("下载 {}", relative), + SYNC_REMOTE_FILE_TIMEOUT, + op.read(&relative), + ) + .await? + .to_vec(); + fs::write(&local_path, bytes) + .map_err(|e| format!("无法写入本地文件 {}: {}", local_path.display(), e))?; + stats.downloaded += 1; + } + SyncAction::DeleteLocal => { + let local_path = local_path_for(workspace, &relative); + if local_path.is_file() { + fs::remove_file(&local_path) + .map_err(|e| format!("无法删除本地文件 {}: {}", local_path.display(), e))?; + } + history.files.remove(&relative); + stats.deleted_local += 1; + continue; + } + SyncAction::DeleteRemote => { + opendal_timeout( + &format!("删除远端文件 {}", relative), + SYNC_REMOTE_FILE_TIMEOUT, + op.delete(&relative), + ) + .await?; + history.files.remove(&relative); + stats.deleted_remote += 1; + continue; + } + SyncAction::Skip => { + stats.skipped += 1; + } + } + } + + let refreshed_local = scan_local_files(workspace)?; + let refreshed_remote = scan_remote_files(&op).await?; + history.files.clear(); + + let synced_paths: BTreeSet = refreshed_local + .keys() + .chain(refreshed_remote.keys()) + .cloned() + .collect(); + + for relative in synced_paths { + let local = refreshed_local.get(&relative); + let remote = refreshed_remote.get(&relative); + let size = local + .map(|item| item.size) + .or_else(|| remote.map(|item| item.size)) + .unwrap_or(0); + history.files.insert( + relative, + SyncHistoryEntry { + local_modified_ms: local.and_then(|item| item.modified_ms), + remote_modified_ms: remote.and_then(|item| item.modified_ms), + size, + }, + ); + } + + save_sync_history(app, &history)?; + + let last_sync_at = now_string(); + let mut saved_config = config.clone(); + saved_config.last_sync_at = Some(last_sync_at.clone()); + save_sync_config_to_disk(app, &saved_config)?; + + Ok(SyncResult { + message: "同步完成".to_string(), + last_sync_at, + stats, + }) +} + +#[tauri::command] +fn load_sync_config(app: tauri::AppHandle) -> Result { + load_sync_config_from_disk(&app) +} + +#[tauri::command] +fn save_sync_config(app: tauri::AppHandle, config: SyncConfig) -> Result<(), String> { + save_sync_config_to_disk(&app, &config) +} + +#[tauri::command] +async fn test_sync_connection(config: SyncConfig) -> Result { + run_sync_connection_test(&config).await +} + +#[tauri::command] +async fn sync_now(app: tauri::AppHandle, workspace: String, config: SyncConfig) -> Result { + let workspace = PathBuf::from(workspace); + sync_workspace(&app, &workspace, &config).await +} + #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) @@ -480,7 +1331,11 @@ pub fn run() { delete_entry, rename_entry, save_media_file, - search_files + search_files, + load_sync_config, + save_sync_config, + test_sync_connection, + sync_now ]) .setup(|app| { // ---- File menu ---- diff --git a/src/App.vue b/src/App.vue index 9866868..1ae73ce 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,6 +14,7 @@ const charCount = ref(0); type DocumentMode = "preview" | "edit" | "mixed"; type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray"; type WorkspaceFileType = "markdown" | "image" | "video"; +type SyncProvider = "none" | "webdav" | "s3"; const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode"; const APP_THEME_KEY = "yurou:app-theme"; const MARKDOWN_EXTENSIONS = new Set(["md"]); @@ -104,6 +105,41 @@ interface WorkspaceWatchErrorPayload { message: string; } +interface SyncConfig { + provider: SyncProvider; + webdav: { + endpoint: string; + username: string; + password: string; + remoteDir: string; + }; + s3: { + endpoint: string; + region: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + remotePrefix: string; + pathStyle: boolean; + useHttps: boolean; + }; + autoSyncOnStartup: boolean; + autoSyncAfterSave: boolean; + lastSyncAt: string | null; +} + +interface SyncResult { + message: string; + lastSyncAt: string; + stats: { + uploaded: number; + downloaded: number; + deletedLocal: number; + deletedRemote: number; + skipped: number; + }; +} + const folderPath = ref(""); const dirEntries = ref([]); const LAST_WORKSPACE_PATH_KEY = "yurou:last-workspace-path"; @@ -115,6 +151,7 @@ const WORKSPACE_REFRESH_DELAY_MS = 140; let workspaceRefreshTimer: ReturnType | null = null; let workspaceRefreshInFlight = false; let workspaceRefreshQueued = false; +let syncInFlight = false; // ---- editor zoom (Ctrl+scroll) ---- const ZOOM_MIN = 0.7; @@ -172,7 +209,7 @@ const documentMode = ref(defaultDocumentMode.value); const appTheme = ref(loadAppTheme()); const showMoreMenu = ref(false); const settingsRequest = ref(0); -const settingsInitialPage = ref<"about" | "editor">("about"); +const settingsInitialPage = ref<"about" | "editor" | "sync">("about"); const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined; scrollToLine: (line: number) => void } | null>(null); const directorySidebarRef = ref<{ activateSearch: () => void } | null>(null); const isWindowMaximized = ref(false); @@ -279,7 +316,7 @@ function onTitlebarDoubleClick(event: MouseEvent) { void toggleWindowMaximize(); } -function openSettings(page: "about" | "editor" = "about") { +function openSettings(page: "about" | "editor" | "sync" = "about") { settingsInitialPage.value = page; settingsRequest.value += 1; } @@ -565,7 +602,7 @@ async function saveCurrentWorkspaceDocumentBeforeSwitch() { const contentToSave = getLatestEditorContent(); if (!currentFilePath.value && !contentToSave.trim()) return true; - return doSave(contentToSave); + return doSave(contentToSave, { syncAfterSave: false }); } function resetWorkspaceAndEditorState(options: { stopWatcher?: boolean } = {}) { @@ -631,14 +668,109 @@ async function switchWorkspace(path: string, options: { persist?: boolean; reset return true; } +async function loadSyncConfig(): Promise { + try { + return await invoke("load_sync_config"); + } catch (e) { + console.error("Failed to load sync config:", e); + return null; + } +} + +function shouldRunSync(config: SyncConfig | null, trigger: "startup" | "save") { + if (!config || config.provider === "none") return false; + if (trigger === "startup") return config.autoSyncOnStartup; + return config.autoSyncAfterSave; +} + +function describeSyncResult(result: SyncResult) { + const { stats } = result; + return `同步完成: 上传 ${stats.uploaded}, 下载 ${stats.downloaded}, 删除本地 ${stats.deletedLocal}, 删除远端 ${stats.deletedRemote}`; +} + +async function refreshCurrentOpenFileFromDisk() { + if (!currentFilePath.value) return; + if (!folderPath.value) return; + + try { + if (isMarkdownDocument.value) { + const fileContent = await invoke("read_file", { path: currentFilePath.value }); + setDocumentContent(fileContent); + hasDraftDocument.value = false; + editorResetKey.value += 1; + return; + } + + if (currentFileType.value === "image" || currentFileType.value === "video") { + mediaPreviewSrc.value = getLocalAssetSrc(currentFilePath.value); + mediaPreviewError.value = ""; + mediaPreviewKey.value += 1; + } + } catch (e) { + console.error("Failed to refresh current file after sync:", e); + resetMarkdownEditorState(); + } +} + +async function runConfiguredSync(trigger: "startup" | "save" | "manual") { + if (!folderPath.value || syncInFlight) return false; + + const config = await loadSyncConfig(); + if (trigger !== "manual" && !shouldRunSync(config, trigger)) return false; + if (!config || config.provider === "none") return false; + + if (trigger !== "save") { + const saved = await saveCurrentWorkspaceDocumentBeforeSwitch(); + if (!saved) return false; + } + + syncInFlight = true; + try { + const result = await invoke("sync_now", { + workspace: folderPath.value, + config, + }); + saveStatus.value = describeSyncResult(result); + setTimeout(() => { + saveStatus.value = ""; + }, 3600); + await refreshWorkspaceTree(); + await refreshCurrentOpenFileFromDisk(); + return true; + } catch (e) { + console.error("Sync failed:", e); + saveStatus.value = `同步失败: ${e}`; + return false; + } finally { + syncInFlight = false; + } +} + +async function handleWorkspaceSynced() { + await refreshWorkspaceTree(); + await refreshCurrentOpenFileFromDisk(); +} + +async function prepareWorkspaceSync(done: (saved: boolean) => void) { + try { + done(await saveCurrentWorkspaceDocumentBeforeSwitch()); + } catch (e) { + console.error("Failed to prepare workspace sync:", e); + done(false); + } +} + async function restoreLastWorkspace() { const lastWorkspacePath = getLastWorkspacePath(); if (!lastWorkspacePath) return; - await switchWorkspace(lastWorkspacePath, { + const restored = await switchWorkspace(lastWorkspacePath, { persist: false, resetEditor: true, }); + if (restored) { + void runConfiguredSync("startup"); + } } async function onOpenFolder() { @@ -877,7 +1009,7 @@ function getLatestEditorContent(contentOverride?: string) { return markdownEditorRef.value?.flushPendingChanges() ?? content.value; } -async function doSave(contentOverride?: string) { +async function doSave(contentOverride?: string, options: { syncAfterSave?: boolean } = {}) { if (!isMarkdownDocument.value) return false; const contentToSave = getLatestEditorContent(contentOverride); @@ -901,6 +1033,9 @@ async function doSave(contentOverride?: string) { saveStatus.value = ""; }, 3000); hasDraftDocument.value = false; + if (options.syncAfterSave !== false) { + void runConfiguredSync("save"); + } return true; } catch (e) { console.error("Save failed:", e); @@ -923,6 +1058,8 @@ async function doSaveAs() { setTimeout(() => { saveStatus.value = ""; }, 3000); + hasDraftDocument.value = false; + void runConfiguredSync("save"); } catch (e) { console.error("Save failed:", e); saveStatus.value = `保存失败: ${e}`; @@ -1023,6 +1160,8 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) { @search-result-click="handleSearchResultClick" @update-default-document-mode="updateDefaultDocumentMode" @update-app-theme="updateAppTheme" + @prepare-workspace-sync="prepareWorkspaceSync" + @workspace-synced="handleWorkspaceSynced" /> diff --git a/src/components/DirectorySidebar.vue b/src/components/DirectorySidebar.vue index c582f3d..1f8aed3 100644 --- a/src/components/DirectorySidebar.vue +++ b/src/components/DirectorySidebar.vue @@ -5,7 +5,7 @@ import SettingsModal from "./SettingsModal.vue"; import TreeEntry from "./TreeEntry.vue"; import PromptDialog from "./PromptDialog.vue"; -type SettingsPage = "about" | "editor"; +type SettingsPage = "about" | "editor" | "sync"; type DocumentMode = "preview" | "edit" | "mixed"; type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray"; @@ -55,9 +55,15 @@ const emit = defineEmits<{ renameEntry: [path: string, newName: string]; updateDefaultDocumentMode: [mode: DocumentMode]; updateAppTheme: [theme: AppTheme]; + prepareWorkspaceSync: [done: (saved: boolean) => void]; + workspaceSynced: []; "search-result-click": [filePath: string, lineNumber: number]; }>(); +function forwardPrepareWorkspaceSync(done: (saved: boolean) => void) { + emit("prepareWorkspaceSync", done); +} + // ---- sidebar view mode ---- type ViewMode = "folder" | "search" | "setting"; const activeView = ref("folder"); @@ -833,10 +839,13 @@ function handleCreateRootFile() { import { getVersion } from "@tauri-apps/api/app"; +import { invoke } from "@tauri-apps/api/core"; import { computed, onBeforeUnmount, ref, watch } from "vue"; -type SettingsPage = "about" | "editor" | "appearance"; +type SettingsPage = "about" | "editor" | "appearance" | "sync"; type DocumentMode = "preview" | "edit" | "mixed"; type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray"; +type SyncProvider = "none" | "webdav" | "s3"; + +interface SyncConfig { + provider: SyncProvider; + webdav: { + endpoint: string; + username: string; + password: string; + remoteDir: string; + }; + s3: { + endpoint: string; + region: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + remotePrefix: string; + pathStyle: boolean; + useHttps: boolean; + }; + autoSyncOnStartup: boolean; + autoSyncAfterSave: boolean; + lastSyncAt: string | null; +} + +interface SyncResult { + message: string; + lastSyncAt: string; + stats: { + uploaded: number; + downloaded: number; + deletedLocal: number; + deletedRemote: number; + skipped: number; + }; +} + +interface SyncConnectionTestResult { + message: string; + details: string[]; +} const props = withDefaults( defineProps<{ show: boolean; initialPage?: SettingsPage; + workspacePath?: string; defaultDocumentMode?: DocumentMode; appTheme?: AppTheme; }>(), { initialPage: "about", + workspacePath: "", defaultDocumentMode: "edit", appTheme: "warm", }, @@ -24,6 +68,8 @@ const emit = defineEmits<{ "update:show": [value: boolean]; updateDefaultDocumentMode: [mode: DocumentMode]; updateAppTheme: [theme: AppTheme]; + prepareWorkspaceSync: [done: (saved: boolean) => void]; + workspaceSynced: []; }>(); const APP_NAME = "yurou"; @@ -36,14 +82,23 @@ const appVersion = ref(FALLBACK_VERSION); const autoUpdate = ref(loadBooleanPref(AUTO_UPDATE_KEY, true)); const language = ref(loadStringPref(LANGUAGE_KEY, "zh-Hans")); const updateStatus = ref(""); +const syncConfig = ref(createDefaultSyncConfig()); +const syncStatus = ref(""); +const syncError = ref(""); +const syncSaving = ref(false); +const syncTesting = ref(false); +const syncRunning = ref(false); let updateStatusTimer: ReturnType | null = null; const pageTitle = computed(() => { if (activePage.value === "about") return "关于"; if (activePage.value === "editor") return "编辑器"; + if (activePage.value === "sync") return "同步"; return "外观"; }); +const isSyncBusy = computed(() => syncSaving.value || syncTesting.value || syncRunning.value); + const menuItems: Array<{ id: SettingsPage; label: string; @@ -52,6 +107,7 @@ const menuItems: Array<{ { id: "about", label: "关于", icon: "ri-information-line" }, { id: "editor", label: "编辑器", icon: "ri-edit-2-line" }, { id: "appearance", label: "外观", icon: "ri-palette-line" }, + { id: "sync", label: "同步", icon: "ri-loop-left-line" }, ]; const documentModeOptions: Array<{ @@ -103,6 +159,49 @@ function savePref(key: string, value: string) { } } +function createDefaultSyncConfig(): SyncConfig { + return { + provider: "none", + webdav: { + endpoint: "", + username: "", + password: "", + remoteDir: "/yurou", + }, + s3: { + endpoint: "", + region: "us-east-1", + bucket: "", + accessKeyId: "", + secretAccessKey: "", + remotePrefix: "yurou", + pathStyle: true, + useHttps: true, + }, + autoSyncOnStartup: false, + autoSyncAfterSave: false, + lastSyncAt: null, + }; +} + +function mergeSyncConfig(config: Partial | null | undefined): SyncConfig { + const defaults = createDefaultSyncConfig(); + if (!config) return defaults; + + return { + ...defaults, + ...config, + webdav: { + ...defaults.webdav, + ...(config.webdav || {}), + }, + s3: { + ...defaults.s3, + ...(config.s3 || {}), + }, + }; +} + function close() { emit("update:show", false); } @@ -149,6 +248,87 @@ function resetTheme() { emit("updateAppTheme", "warm"); } +function setSyncStatus(message: string, isError = false) { + if (isError) { + syncError.value = message; + syncStatus.value = ""; + } else { + syncStatus.value = message; + syncError.value = ""; + } +} + +async function loadSyncConfig() { + try { + const config = await invoke("load_sync_config"); + syncConfig.value = mergeSyncConfig(config); + syncError.value = ""; + } catch (error) { + setSyncStatus(`读取同步配置失败: ${error}`, true); + } +} + +async function saveSyncConfig() { + syncSaving.value = true; + try { + await invoke("save_sync_config", { config: syncConfig.value }); + setSyncStatus("同步配置已保存"); + } catch (error) { + setSyncStatus(`保存失败: ${error}`, true); + } finally { + syncSaving.value = false; + } +} + +async function testSyncConnection() { + syncTesting.value = true; + try { + const result = await invoke("test_sync_connection", { config: syncConfig.value }); + setSyncStatus([result.message, ...result.details.map((item) => `- ${item}`)].join("\n")); + } catch (error) { + setSyncStatus(String(error), true); + } finally { + syncTesting.value = false; + } +} + +function formatSyncResult(result: SyncResult) { + const { stats } = result; + return `同步完成:上传 ${stats.uploaded},下载 ${stats.downloaded},删除本地 ${stats.deletedLocal},删除远端 ${stats.deletedRemote},跳过 ${stats.skipped}`; +} + +async function runSyncNow() { + if (!props.workspacePath) { + setSyncStatus("请先打开一个工作目录", true); + return; + } + + syncRunning.value = true; + try { + setSyncStatus("正在同步:扫描本地和远端文件,远端列表请求最长等待 120 秒。"); + await invoke("save_sync_config", { config: syncConfig.value }); + const prepared = await new Promise((resolve) => { + emit("prepareWorkspaceSync", resolve); + }); + if (!prepared) { + setSyncStatus("同步已取消,当前文档尚未保存", true); + return; + } + + const result = await invoke("sync_now", { + workspace: props.workspacePath, + config: syncConfig.value, + }); + syncConfig.value.lastSyncAt = result.lastSyncAt; + setSyncStatus(formatSyncResult(result)); + emit("workspaceSynced"); + } catch (error) { + setSyncStatus(`同步失败: ${error}`, true); + } finally { + syncRunning.value = false; + } +} + watch( () => props.show, (show) => { @@ -156,6 +336,7 @@ watch( activePage.value = props.initialPage; updateStatus.value = ""; void loadAppVersion(); + void loadSyncConfig(); }, ); @@ -185,7 +366,6 @@ onBeforeUnmount(() => { v-if="show" class="settings-overlay" role="presentation" - @click.self="close" @keydown="onOverlayKeydown" >
{
+
+
+
+
+

同步方式

+

同步当前工作目录中的所有文件,包括隐藏文件。

+
+ +
+ + + + + +
+
+

同步策略

+

启动时和保存后可自动同步,冲突时以最新修改为准。

+
+
+ + +
+
+ +
+
+

立即同步

+

+ {{ syncConfig.lastSyncAt ? `上次同步:${syncConfig.lastSyncAt}` : '尚未同步' }} +

+
+ {{ syncError || syncStatus }} +
+
+
+ + + +
+
+
+
+
@@ -372,7 +760,9 @@ onBeforeUnmount(() => { .settings-shell { display: grid; grid-template-columns: 176px minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr); width: min(820px, calc(100vw - 48px)); + height: min(680px, calc(100vh - 48px)); min-height: 430px; max-height: min(680px, calc(100vh - 48px)); overflow: hidden; @@ -433,6 +823,7 @@ onBeforeUnmount(() => { .settings-main { display: flex; + min-height: 0; min-width: 0; flex-direction: column; background: var(--app-panel-bg); @@ -480,9 +871,14 @@ onBeforeUnmount(() => { min-height: 0; flex: 1; overflow-y: auto; + overscroll-behavior: contain; padding: 22px; } +.settings-sync-content { + scrollbar-gutter: stable; +} + .settings-about-card { overflow: hidden; border-radius: 10px; @@ -592,6 +988,21 @@ onBeforeUnmount(() => { background: var(--app-surface-muted); } +.settings-primary-btn:disabled, +.settings-secondary-btn:disabled { + cursor: default; + opacity: 0.56; + box-shadow: none; +} + +.settings-primary-btn:disabled:hover { + background: var(--app-control); +} + +.settings-secondary-btn:disabled:hover { + background: var(--app-surface); +} + .settings-status { color: var(--app-muted); font-size: 11px; @@ -665,6 +1076,95 @@ onBeforeUnmount(() => { box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-accent) 12%, transparent); } +.settings-form-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + padding: 16px 20px; + border-top: 1px solid var(--app-border-strong); +} + +.settings-field { + display: flex; + min-width: 0; + flex-direction: column; + gap: 7px; +} + +.settings-field > span { + color: var(--app-text-soft); + font-size: 12px; + font-weight: 600; + line-height: 1.2; +} + +.settings-input { + width: 100%; + min-width: 0; + height: 32px; + border: 1px solid var(--app-border-strong); + border-radius: 6px; + background: var(--app-surface); + color: var(--app-text); + font-size: 13px; + line-height: 1; + padding: 0 10px; + box-shadow: 0 1px 4px color-mix(in srgb, var(--app-shadow) 48%, transparent); + outline: none; +} + +.settings-input::placeholder { + color: var(--app-subtle); +} + +.settings-input:focus { + border-color: var(--app-accent-muted); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-accent) 12%, transparent); +} + +.settings-switch-group { + display: flex; + min-width: 164px; + flex-direction: column; + gap: 10px; +} + +.settings-switch-line { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + color: var(--app-text-soft); + font-size: 12px; + line-height: 1; +} + +.settings-sync-actions { + align-items: flex-start; +} + +.settings-sync-log { + width: min(100%, 420px); + max-height: 138px; + margin-top: 10px; + overflow: auto; + border: 1px solid color-mix(in srgb, var(--app-accent) 26%, var(--app-border)); + border-radius: 6px; + background: color-mix(in srgb, var(--app-accent) 7%, var(--app-surface)); + color: var(--app-accent); + font-size: 12px; + line-height: 1.5; + padding: 9px 10px; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.settings-sync-log-error { + border-color: color-mix(in srgb, #dc2626 32%, var(--app-border)); + background: color-mix(in srgb, #dc2626 7%, var(--app-surface)); + color: #dc2626; +} + .theme-swatch-group { display: inline-flex; align-items: center; @@ -727,8 +1227,11 @@ onBeforeUnmount(() => { .settings-shell { grid-template-columns: 1fr; + grid-template-rows: auto minmax(0, 1fr); width: 100%; - max-height: none; + height: calc(100vh - 28px); + max-height: calc(100vh - 28px); + min-height: 0; } .settings-menu { @@ -751,8 +1254,16 @@ onBeforeUnmount(() => { gap: 12px; } + .settings-form-row { + grid-template-columns: 1fr; + } + .settings-action-stack { align-items: flex-start; } + + .settings-switch-group { + min-width: 0; + } }