diff --git a/Cargo.lock b/Cargo.lock index 1e946e8..a630128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -30,15 +30,15 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.2.15", + "getrandom 0.3.3", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -73,9 +73,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -88,44 +88,47 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +dependencies = [ + "backtrace", +] [[package]] name = "arc-swap" @@ -169,7 +172,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "synstructure", ] @@ -181,7 +184,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -209,26 +212,15 @@ dependencies = [ "tokio", ] -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -242,6 +234,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -261,9 +262,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -275,7 +276,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body", "http-body-util", "hyper", @@ -307,7 +308,7 @@ checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", "futures-core", - "http 1.2.0", + "http 1.3.1", "http-body", "http-body-util", "mime", @@ -321,9 +322,9 @@ dependencies = [ [[package]] name = "backon" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fef586913a57ff189f25c9b3d034356a5bf6b3fa9a7f067588fe1698ba1f5d" +checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7" dependencies = [ "fastrand", "gloo-timers", @@ -332,9 +333,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -359,7 +360,9 @@ dependencies = [ "positioned-io", "range-collections", "self_cell", + "serde", "smallvec", + "tokio", ] [[package]] @@ -382,9 +385,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "binary-merge" @@ -415,9 +418,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "blake3" @@ -447,11 +450,17 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "102dbef1187b1893e6dfe05a774e79fd52265f49f214f6879c8ff49f52c8188b" +[[package]] +name = "btparse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387e80962b798815a2b5c4bcfdb6bf626fa922ffe9f74e373103b858738e9f31" + [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -461,18 +470,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] [[package]] name = "camino" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" dependencies = [ "serde", ] @@ -501,9 +510,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.14" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "shlex", ] @@ -516,9 +525,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -539,9 +548,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -549,7 +558,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -565,9 +574,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.29" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -575,9 +584,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.29" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -587,43 +596,47 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cobs" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.12", +] [[package]] -name = "colorchoice" -version = "1.0.3" +name = "color-backtrace" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "2123a5984bd52ca861c66f66a9ab9883b27115c607f801f86c1bc2a84eb69f0f" +dependencies = [ + "backtrace", + "btparse", + "termcolor", +] [[package]] -name = "colored" -version = "2.2.0" +name = "colorchoice" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.59.0", -] +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -644,19 +657,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -671,11 +671,11 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "cordyceps" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec10f0a762d93c4498d2e97a333805cb6250d60bead623f71d8034f9a4152ba3" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" dependencies = [ - "loom 0.5.6", + "loom", "tracing", ] @@ -691,9 +691,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -716,9 +716,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -759,18 +759,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -840,7 +828,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -859,15 +847,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "der_derive", @@ -897,25 +885,46 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] +[[package]] +name = "derive-ex" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba95f299f6b9cd47f68a847eca2ae9060a2713af532dc35c342065544845407" +dependencies = [ + "proc-macro2", + "quote", + "structmeta", + "syn 2.0.104", +] + [[package]] name = "derive_more" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -926,21 +935,20 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "unicode-xid", ] [[package]] -name = "dialoguer" -version = "0.11.0" +name = "derive_more-impl" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "console", - "shell-words", - "tempfile", - "thiserror 1.0.69", - "zeroize", + "proc-macro2", + "quote", + "syn 2.0.104", + "unicode-xid", ] [[package]] @@ -956,32 +964,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -990,7 +976,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1006,9 +992,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" dependencies = [ "litrs", ] @@ -1019,20 +1005,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -1059,25 +1031,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "embedded-io" version = "0.4.0" @@ -1090,12 +1043,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -1105,27 +1052,27 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "enumflags2" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1136,12 +1083,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1157,9 +1104,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", @@ -1177,16 +1124,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "ff" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1199,18 +1136,6 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1219,9 +1144,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" @@ -1329,7 +1254,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1401,28 +1326,16 @@ dependencies = [ [[package]] name = "generator" -version = "0.7.5" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" dependencies = [ "cc", - "libc", - "log", - "rustversion", - "windows 0.48.0", -] - -[[package]] -name = "generator" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" -dependencies = [ "cfg-if", "libc", "log", "rustversion", - "windows 0.58.0", + "windows 0.61.3", ] [[package]] @@ -1438,14 +1351,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1504,35 +1417,24 @@ dependencies = [ "parking_lot", "portable-atomic", "quanta", - "rand 0.9.0", + "rand 0.9.1", "smallvec", "spinning_top", "web-time", ] -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "h2" -version = "0.4.7" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.2.0", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -1540,20 +1442,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -1562,24 +1470,32 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.4", ] [[package]] -name = "heck" -version = "0.5.0" +name = "heapless" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" @@ -1589,14 +1505,12 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-proto" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d844af74f7b799e41c78221be863bade11c430d46042c3b49ca8ae0c6d27287" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ - "async-recursion", "async-trait", "cfg-if", - "critical-section", "data-encoding", "enum-as-inner", "futures-channel", @@ -1605,9 +1519,9 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.0", + "rand 0.9.1", "ring", - "thiserror 2.0.11", + "thiserror 2.0.12", "tinyvec", "tokio", "tracing", @@ -1616,9 +1530,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a128410b38d6f931fcc6ca5c107a3b02cabd6c05967841269a4ad65d23c44331" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", @@ -1627,10 +1541,10 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.0", + "rand 0.9.1", "resolv-conf", "smallvec", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", ] @@ -1656,20 +1570,9 @@ dependencies = [ [[package]] name = "hmac-sha256" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a8575493d277c9092b988c780c94737fb9fd8651a1001e16bee3eccfc1baedb" - -[[package]] -name = "hostname" -version = "0.3.1" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi", -] +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" [[package]] name = "hostname-validator" @@ -1690,9 +1593,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1706,27 +1609,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http 1.3.1", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.2.0", + "futures-core", + "http 1.3.1", "http-body", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1744,7 +1647,7 @@ dependencies = [ "futures-channel", "futures-util", "h2", - "http 1.2.0", + "http 1.3.1", "http-body", "httparse", "httpdate", @@ -1757,12 +1660,11 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", - "http 1.2.0", + "http 1.3.1", "hyper", "hyper-util", "rustls", @@ -1770,22 +1672,26 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.1", ] [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -1795,16 +1701,17 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.2", ] [[package]] @@ -1818,21 +1725,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1841,31 +1749,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1873,67 +1761,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "idna" version = "1.0.3" @@ -1947,9 +1822,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1965,12 +1840,12 @@ dependencies = [ "attohttpc", "bytes", "futures", - "http 1.2.0", + "http 1.3.1", "http-body-util", "hyper", "hyper-util", "log", - "rand 0.9.0", + "rand 0.9.1", "tokio", "url", "xmltree", @@ -1978,33 +1853,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.2", -] - -[[package]] -name = "indicatif" -version = "0.17.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" -dependencies = [ - "console", - "number_prefix", - "portable-atomic", - "tokio", - "unicode-width", - "web-time", + "hashbrown 0.15.4", ] [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "generic-array", ] @@ -2048,30 +1909,37 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "iroh" -version = "0.35.0" +version = "0.90.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ca758f4ce39ae3f07de922be6c73de6a48a07f39554e78b5745585652ce38f5" +checksum = "9436f319c2d24bca1b28a2fab4477c8d2ac795ab2d3aeda142d207b38ec068f4" dependencies = [ "aead", - "anyhow", - "atomic-waker", "axum", "backon", "bytes", "cfg_aliases", - "concurrent-queue", "crypto_box", "data-encoding", "der", - "derive_more", + "derive_more 1.0.0", "ed25519-dalek", "futures-buffered", "futures-util", "getrandom 0.3.3", "hickory-resolver", - "http 1.2.0", + "http 1.3.1", "igd-next", "instant", "iroh-base", @@ -2081,24 +1949,27 @@ dependencies = [ "iroh-quinn-udp", "iroh-relay", "n0-future", + "n0-snafu", + "n0-watcher", + "nested_enum_utils 0.2.2", "netdev", "netwatch", "pin-project", "pkarr", "portmapper", "rand 0.8.5", - "rcgen", "reqwest", "ring", "rustls", + "rustls-pki-types", "rustls-webpki", "serde", "smallvec", + "snafu", "spki", "strum", "stun-rs", "surge-ping", - "thiserror 2.0.11", "time", "tokio", "tokio-stream", @@ -2106,58 +1977,43 @@ dependencies = [ "tracing", "url", "wasm-bindgen-futures", - "webpki-roots", - "x509-parser", + "webpki-roots 0.26.11", "z32", ] [[package]] name = "iroh-base" -version = "0.35.0" +version = "0.90.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91ac4aaab68153d726c4e6b39c30f9f9253743f0e25664e52f4caeb46f48d11" +checksum = "8e0090050c4055b21e61cbcb856f043a2b24ad22c65d76bab91f121b4c7bece3" dependencies = [ "curve25519-dalek", "data-encoding", - "derive_more", + "derive_more 1.0.0", "ed25519-dalek", + "n0-snafu", + "nested_enum_utils 0.2.2", "postcard", "rand_core 0.6.4", "serde", - "thiserror 2.0.11", + "snafu", "url", ] -[[package]] -name = "iroh-blake3" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbba31f40a650f58fa28dd585a8ca76d8ae3ba63aacab4c8269004a0c803930" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - [[package]] name = "iroh-blobs" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "817b785193b73c34ef1f2dcb5ddf8729ecef9b72a8fc0e706ee6d7a9bf8766a6" +version = "0.90.0" +source = "git+https://github.com/n0-computer/iroh-blobs?branch=Frando/gc-protect#efa928e3d983a73ed682c84448774616aeb0351f" dependencies = [ "anyhow", - "async-channel", + "arrayvec", "bao-tree", - "blake3", "bytes", "chrono", "data-encoding", - "derive_more", + "derive_more 2.0.1", "futures-buffered", "futures-lite", - "futures-util", "genawaiter", "hashlink", "hex", @@ -2165,32 +2021,24 @@ dependencies = [ "iroh-base", "iroh-io", "iroh-metrics", - "nested_enum_utils 0.1.0", - "num_cpus", - "oneshot", - "parking_lot", - "portable-atomic", + "iroh-quinn", + "irpc", + "n0-future", + "n0-snafu", + "nested_enum_utils 0.2.2", "postcard", - "quic-rpc", - "quic-rpc-derive", "rand 0.8.5", "range-collections", "redb 2.4.0", + "ref-cast", "reflink-copy", "self_cell", "serde", - "serde-error", "smallvec", - "ssh-key", - "strum", - "tempfile", - "thiserror 2.0.11", + "snafu", "tokio", "tokio-util", "tracing", - "tracing-futures", - "tracing-test", - "walkdir", ] [[package]] @@ -2199,34 +2047,28 @@ version = "0.35.0" dependencies = [ "anyhow", "async-channel", + "blake3", "bytes", - "clap", - "colored", - "console", "data-encoding", - "derive_more", - "dialoguer", + "derive_more 1.0.0", "ed25519-dalek", "futures-buffered", "futures-lite", "futures-util", "hex", - "indicatif", "iroh", "iroh-base", - "iroh-blake3", "iroh-blobs", "iroh-gossip", - "iroh-io", "iroh-metrics", + "iroh-quinn", + "irpc", + "n0-future", "nested_enum_utils 0.1.0", "num_enum", "parking_lot", - "portable-atomic", "postcard", "proptest", - "quic-rpc", - "quic-rpc-derive", "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -2235,13 +2077,12 @@ dependencies = [ "self_cell", "serde", "serde-error", - "shellexpand", "strum", "tempfile", "test-strategy", "testdir", "testresult", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-stream", "tokio-util", @@ -2252,14 +2093,14 @@ dependencies = [ [[package]] name = "iroh-gossip" -version = "0.35.0" +version = "0.90.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca43045ceb44b913369f417d56323fb1628ebf482ab4c1e9360e81f1b58cbc2" +checksum = "1b6282651cd1b24923803295a56532a379d815aaa838a432f6bff6919b082b3e" dependencies = [ - "anyhow", - "async-channel", + "blake3", "bytes", - "derive_more", + "data-encoding", + "derive_more 1.0.0", "ed25519-dalek", "futures-concurrency", "futures-lite", @@ -2267,15 +2108,18 @@ dependencies = [ "hex", "indexmap", "iroh", - "iroh-blake3", + "iroh-base", "iroh-metrics", + "irpc", "n0-future", + "n0-snafu", + "n0-watcher", + "nested_enum_utils 0.2.2", "postcard", "rand 0.8.5", "rand_core 0.6.4", "serde", - "serde-error", - "thiserror 2.0.11", + "snafu", "tokio", "tokio-util", "tracing", @@ -2296,15 +2140,16 @@ dependencies = [ [[package]] name = "iroh-metrics" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70466f14caff7420a14373676947e25e2917af6a5b1bec45825beb2bf1eb6a7" +checksum = "c8922c169f1b84d39d325c02ef1bbe1419d4de6e35f0403462b3c7e60cc19634" dependencies = [ "http-body-util", "hyper", "hyper-util", "iroh-metrics-derive", "itoa", + "postcard", "reqwest", "serde", "snafu", @@ -2321,14 +2166,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "iroh-quinn" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c6245c9ed906506ab9185e8d7f64857129aee4f935e899f398a3bd3b70338d" +checksum = "0cde160ebee7aabede6ae887460cd303c8b809054224815addf1469d54a6fcf7" dependencies = [ "bytes", "cfg_aliases", @@ -2338,7 +2183,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "web-time", @@ -2351,7 +2196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535" dependencies = [ "bytes", - "getrandom 0.2.15", + "getrandom 0.2.16", "rand 0.8.5", "ring", "rustc-hash", @@ -2359,7 +2204,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 2.0.11", + "thiserror 2.0.12", "tinyvec", "tracing", "web-time", @@ -2381,23 +2226,22 @@ dependencies = [ [[package]] name = "iroh-relay" -version = "0.35.0" +version = "0.90.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c63f122cdfaa4b4e0e7d6d3921d2b878f42a0c6d3ee5a29456dc3f5ab5ec931f" +checksum = "c3f3cdbdaebc92835452e4e1d0d4b36118206b0950089b7bc3654f13e843475b" dependencies = [ "ahash", - "anyhow", "bytes", "cfg_aliases", "clap", "dashmap", "data-encoding", - "derive_more", + "derive_more 1.0.0", "getrandom 0.3.3", "governor", "hickory-proto", "hickory-resolver", - "http 1.2.0", + "http 1.3.1", "http-body-util", "hyper", "hyper-util", @@ -2405,8 +2249,10 @@ dependencies = [ "iroh-metrics", "iroh-quinn", "iroh-quinn-proto", - "lru 0.12.5", + "lru", "n0-future", + "n0-snafu", + "nested_enum_utils 0.2.2", "num_enum", "pin-project", "pkarr", @@ -2420,13 +2266,13 @@ dependencies = [ "rustls-cert-file-reader", "rustls-cert-reloadable-resolver", "rustls-pemfile", + "rustls-pki-types", "rustls-webpki", "serde", "sha1", "simdutf8", + "snafu", "strum", - "stun-rs", - "thiserror 2.0.11", "time", "tokio", "tokio-rustls", @@ -2437,11 +2283,45 @@ dependencies = [ "tracing", "tracing-subscriber", "url", - "webpki-roots", + "webpki-roots 0.26.11", "ws_stream_wasm", "z32", ] +[[package]] +name = "irpc" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b355fe12226ee885e1c1056a867c2cf37be2b22032a16f5ab7091069e98a966f" +dependencies = [ + "anyhow", + "futures-buffered", + "futures-util", + "iroh-quinn", + "irpc-derive", + "n0-future", + "postcard", + "rcgen", + "rustls", + "serde", + "smallvec", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "irpc-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efeabe1ee5615ea0416340b1a6d71a16f971495859c87fad48633b6497ee7a77" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2450,9 +2330,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jni" @@ -2491,43 +2371,24 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "libm" -version = "0.2.11" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" - -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.8.0", - "libc", -] +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" @@ -2537,9 +2398,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -2547,22 +2408,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" - -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator 0.7.5", - "scoped-tls", - "tracing", - "tracing-subscriber", -] +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "loom" @@ -2571,32 +2419,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ "cfg-if", - "generator 0.8.4", + "generator", "scoped-tls", "tracing", "tracing-subscriber", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.2", -] - [[package]] name = "lru" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +dependencies = [ + "hashbrown 0.15.4", +] [[package]] -name = "match_cfg" -version = "0.1.0" +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "matchers" @@ -2621,9 +2463,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "mime" @@ -2639,22 +2481,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -2666,7 +2508,7 @@ dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom 0.7.2", + "loom", "parking_lot", "portable-atomic", "rustc_version", @@ -2683,7 +2525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794" dependencies = [ "cfg_aliases", - "derive_more", + "derive_more 1.0.0", "futures-buffered", "futures-lite", "futures-util", @@ -2698,12 +2540,27 @@ dependencies = [ ] [[package]] -name = "nanorand" -version = "0.7.0" +name = "n0-snafu" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4fed465ff57041f29db78a9adc8864296ef93c6c16029f9e192dc303404ebd0" +dependencies = [ + "anyhow", + "btparse", + "color-backtrace", + "snafu", + "tracing-error", +] + +[[package]] +name = "n0-watcher" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +checksum = "f216d4ebc5fcf9548244803cbb93f488a2ae160feba3706cd17040d69cf7a368" dependencies = [ - "getrandom 0.2.15", + "derive_more 1.0.0", + "n0-future", + "snafu", ] [[package]] @@ -2779,7 +2636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0800eae8638a299eaa67476e1c6b6692922273e0f7939fd188fc861c837b9cd2" dependencies = [ "anyhow", - "bitflags 2.8.0", + "bitflags 2.9.1", "byteorder", "libc", "log", @@ -2810,7 +2667,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2828,24 +2685,26 @@ dependencies = [ [[package]] name = "netwatch" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eeaa5f7505c93c5a9b35ba84fd21fb8aa3f24678c76acfe8716af7862fb07a" +checksum = "2a829a830199b14989f9bccce6136ab928ab48336ab1f8b9002495dbbbb2edbe" dependencies = [ "atomic-waker", "bytes", "cfg_aliases", - "derive_more", + "derive_more 1.0.0", "iroh-quinn-udp", "js-sys", "libc", "n0-future", + "n0-watcher", "nested_enum_utils 0.2.2", "netdev", "netlink-packet-core", "netlink-packet-route 0.23.0", "netlink-proto", "netlink-sys", + "pin-project-lite", "serde", "snafu", "socket2", @@ -2855,7 +2714,7 @@ dependencies = [ "tracing", "web-sys", "windows 0.59.0", - "windows-result 0.3.0", + "windows-result", "wmi", ] @@ -2904,7 +2763,7 @@ checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" dependencies = [ "base32", "document-features", - "getrandom 0.2.15", + "getrandom 0.2.16", "httpdate", "js-sys", "once_cell", @@ -2931,23 +2790,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -2963,17 +2805,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2981,46 +2812,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", ] [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "object" version = "0.36.7" @@ -3041,19 +2856,19 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" dependencies = [ "critical-section", "portable-atomic", ] [[package]] -name = "oneshot" -version = "0.1.10" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d72a7c0f743d2ebb0a2ad1d219db75fdc799092ed3a884c9144c42a31225bd" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opaque-debug" @@ -3067,56 +2882,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p521" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" -dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", - "primeorder", - "rand_core 0.6.4", - "sha2", -] - [[package]] name = "parking" version = "2.2.1" @@ -3125,9 +2896,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -3135,9 +2906,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -3154,9 +2925,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ "base64", "serde", @@ -3179,20 +2950,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.11", + "thiserror 2.0.12", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -3200,24 +2971,23 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "pest_meta" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -3234,22 +3004,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -3266,9 +3036,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32222ae3d617bf92414db29085f8a959a4515effce916e038e9399a335a0d6d" +checksum = "41a50f65a2b97031863fbdff2f085ba832360b4bef3106d1fcff9ab5bf4063fe" dependencies = [ "async-compat", "base32", @@ -3279,33 +3049,22 @@ dependencies = [ "ed25519-dalek", "futures-buffered", "futures-lite", - "getrandom 0.2.15", + "getrandom 0.2.16", "log", - "lru 0.13.0", + "lru", "ntimestamp", "reqwest", "self_cell", "serde", "sha1_smol", "simple-dns", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "url", "wasm-bindgen-futures", ] -[[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 = "pkcs8" version = "0.10.2" @@ -3334,7 +3093,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -3371,19 +3130,19 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portmapper" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6db66007eac4a0ec8331d0d20c734bd64f6445d64bbaf0d0a27fea7a054e36" +checksum = "2d82975dc029c00d566f4e0f61f567d31f0297a290cb5416b5580dd8b4b54ade" dependencies = [ "base64", "bytes", - "derive_more", + "derive_more 1.0.0", "futures-lite", "futures-util", "hyper-util", @@ -3408,9 +3167,9 @@ dependencies = [ [[package]] name = "positioned-io" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccabfeeb89c73adf4081f0dca7f8e28dbda90981a222ceea37f619e93ea6afe9" +checksum = "e8078ce4d22da5e8f57324d985cc9befe40c49ab0507a192d6be9e59584495c9" dependencies = [ "libc", "winapi", @@ -3418,26 +3177,36 @@ dependencies = [ [[package]] name = "postcard" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +checksum = "6c1de96e20f51df24ca73cafcc4690e044854d803259db27a00a461cb3b9d17a" dependencies = [ "cobs", "embedded-io 0.4.0", "embedded-io 0.6.1", + "heapless", "postcard-derive", "serde", ] [[package]] name = "postcard-derive" -version = "0.1.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0239fa9c1d225d4b7eb69925c25c5e082307a141e470573fbbe3a817ce6a7a37" +checksum = "68f049d94cb6dda6938cc8a531d2898e7c08d71c6de63d8e67123cca6cdde2cc" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.104", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", ] [[package]] @@ -3448,11 +3217,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -3489,20 +3258,11 @@ dependencies = [ "ucd-parse", ] -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] @@ -3541,26 +3301,26 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.8.0", + "bitflags 2.9.1", "lazy_static", "num-traits", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.1", + "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax 0.8.5", "rusty-fork", @@ -3570,53 +3330,19 @@ dependencies = [ [[package]] name = "quanta" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] -[[package]] -name = "quic-rpc" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18bad98bd048264ceb1361ff9d77a031535d8c1e3fe8f12c6966ec825bf68eb7" -dependencies = [ - "anyhow", - "document-features", - "flume", - "futures-lite", - "futures-sink", - "futures-util", - "pin-project", - "serde", - "slab", - "smallvec", - "time", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "quic-rpc-derive" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf13f1bced5f2f2642d9d89a29d75f2d81ab34c4acfcb434c209d6094b9b2b7" -dependencies = [ - "proc-macro2", - "quic-rpc", - "quote", - "syn 1.0.109", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -3625,37 +3351,40 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quinn" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", "socket2", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.9" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "getrandom 0.2.15", - "rand 0.8.5", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.11", + "thiserror 2.0.12", "tinyvec", "tracing", "web-time", @@ -3663,9 +3392,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.10" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", @@ -3677,9 +3406,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -3696,9 +3425,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -3713,13 +3442,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.0", - "zerocopy 0.8.18", + "rand_core 0.9.3", ] [[package]] @@ -3739,7 +3467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -3748,47 +3476,47 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] name = "rand_core" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.3", - "zerocopy 0.8.18", ] [[package]] name = "rand_xorshift" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.9.3", ] [[package]] name = "range-collections" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca9edd21e2db51000ac63eccddabba622f826e631a60be7bade9bd6a76b69537" +checksum = "861706ea9c4aded7584c5cd1d241cec2ea7f5f50999f236c22b65409a1f1a0d0" dependencies = [ "binary-merge", "inplace-vec-builder", "ref-cast", + "serde", "smallvec", ] [[package]] name = "raw-cpuid" -version = "11.3.0" +version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6928fa44c097620b706542d428957635951bade7143269085389d42c8a4927e" +checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", ] [[package]] @@ -3824,54 +3552,43 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" -dependencies = [ - "bitflags 2.8.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 1.0.69", + "bitflags 2.9.1", ] [[package]] name = "ref-cast" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "reflink-copy" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd3533fd4222b8337470456ea84d80436b4c91c53db51c372461d5f7e6eb0b4" +checksum = "78c81d000a2c524133cc00d2f92f019d399e57906c3b7119271a2495354fe895" dependencies = [ "cfg-if", "libc", "rustix", - "windows 0.59.0", + "windows 0.61.3", ] [[package]] @@ -3943,30 +3660,26 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "4c8cea6b35bcceb099f30173754403d2eba0a5dc18cea3630fccd88251909288" dependencies = [ "base64", "bytes", "futures-core", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -3976,76 +3689,41 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", - "windows-registry", + "webpki-roots 1.0.1", ] [[package]] name = "resolv-conf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[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.15", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rsa" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "sha2", - "signature", - "spki", - "subtle", - "zeroize", +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" + +[[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.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -4073,11 +3751,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", @@ -4086,9 +3764,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "log", "once_cell", @@ -4108,7 +3786,7 @@ dependencies = [ "rustls-cert-read", "rustls-pemfile", "rustls-pki-types", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -4131,7 +3809,7 @@ dependencies = [ "reloadable-state", "rustls", "rustls-cert-read", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4157,20 +3835,21 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-platform-verifier" -version = "0.5.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e012c45844a1790332c9386ed4ca3a06def221092eda277e6f079728f8ea99da" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ - "core-foundation 0.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -4181,8 +3860,8 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.52.0", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", ] [[package]] @@ -4193,9 +3872,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -4204,9 +3883,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rusty-fork" @@ -4222,9 +3901,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa20" @@ -4265,28 +3944,14 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - [[package]] name = "security-framework" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.8.0", - "core-foundation 0.10.0", + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4304,15 +3969,15 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] @@ -4349,14 +4014,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -4366,9 +4031,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" dependencies = [ "itoa", "serde", @@ -4376,9 +4041,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -4424,9 +4089,9 @@ checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -4442,21 +4107,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - -[[package]] -name = "shellexpand" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" -dependencies = [ - "dirs", -] - [[package]] name = "shlex" version = "1.3.0" @@ -4465,9 +4115,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -4478,7 +4128,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", "rand_core 0.6.4", ] @@ -4494,53 +4143,51 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "snafu" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +checksum = "320b01e011bf8d5d7a4a4a4be966d9160968935849c83b918827f6a435e7f627" dependencies = [ + "backtrace", "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -4574,48 +4221,6 @@ dependencies = [ "der", ] -[[package]] -name = "ssh-cipher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" -dependencies = [ - "cipher", - "ssh-encoding", -] - -[[package]] -name = "ssh-encoding" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" -dependencies = [ - "base64ct", - "pem-rfc7468", - "sha2", -] - -[[package]] -name = "ssh-key" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" -dependencies = [ - "ed25519-dalek", - "p256", - "p384", - "p521", - "rand_core 0.6.4", - "rsa", - "sec1", - "sha2", - "signature", - "ssh-cipher", - "ssh-encoding", - "subtle", - "zeroize", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -4637,7 +4242,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -4648,7 +4253,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -4670,7 +4275,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -4694,7 +4299,7 @@ dependencies = [ "precis-core", "precis-profiles", "quoted-string-parser", - "rand 0.9.0", + "rand 0.9.1", ] [[package]] @@ -4705,14 +4310,14 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "surge-ping" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbf95ce4c7c5b311d2ce3f088af2b93edef0f09727fa50fbe03c7a979afce77" +checksum = "6fda78103d8016bb25c331ddc54af634e801806463682cc3e549d335df644d95" dependencies = [ "hex", "parking_lot", "pnet_packet", - "rand 0.8.5", + "rand 0.9.1", "socket2", "thiserror 1.0.69", "tokio", @@ -4732,9 +4337,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -4763,13 +4368,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -4792,7 +4397,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4815,11 +4420,10 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.16.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", "getrandom 0.3.3", "once_cell", @@ -4827,16 +4431,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "test-strategy" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf41af45e3f54cc184831d629d41d5b2bda8297e29c81add7ae4f362ed5e01b" +checksum = "43b12f9683de37f9980e485167ee624bfaa0b6b04da661e98e25ef9c2669bc1b" dependencies = [ + "derive-ex", "proc-macro2", "quote", "structmeta", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -4870,11 +4484,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -4885,35 +4499,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -4927,15 +4540,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -4943,9 +4556,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -4953,9 +4566,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -4968,14 +4581,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -4991,14 +4605,14 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -5024,11 +4638,11 @@ dependencies = [ "rustls", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-rustls", - "webpki-roots", + "webpki-roots 0.26.11", "x509-parser", ] @@ -5052,9 +4666,10 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "futures-util", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "pin-project-lite", "slab", "tokio", @@ -5062,18 +4677,18 @@ dependencies = [ [[package]] name = "tokio-websockets" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc46f9dc832c663a5db08513162001a29ac820913275d58943f942c2bc1c435" +checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e" dependencies = [ "base64", "bytes", "futures-core", "futures-sink", "getrandom 0.3.3", - "http 1.2.0", + "http 1.3.1", "httparse", - "rand 0.9.0", + "rand 0.9.1", "ring", "rustls-pki-types", "simdutf8", @@ -5084,9 +4699,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -5096,26 +4711,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -5132,6 +4754,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -5158,33 +4798,33 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", ] [[package]] -name = "tracing-futures" -version = "0.2.5" +name = "tracing-error" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" dependencies = [ - "pin-project", "tracing", + "tracing-subscriber", ] [[package]] @@ -5234,7 +4874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -5245,9 +4885,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-parse" @@ -5272,9 +4912,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" @@ -5285,12 +4925,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -5325,12 +4959,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5345,11 +4973,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.13.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -5394,9 +5024,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -5435,7 +5065,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -5470,7 +5100,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5519,27 +5149,45 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.1", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.1", +] + +[[package]] +name = "webpki-roots" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ "redox_syscall", "wasite", @@ -5548,9 +5196,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" @@ -5585,77 +5233,71 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows" -version = "0.58.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-core 0.59.0", + "windows-targets 0.53.2", ] [[package]] name = "windows" -version = "0.59.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.59.0", - "windows-targets 0.53.0", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-collections" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-targets 0.52.6", + "windows-core 0.61.2", ] [[package]] name = "windows-core" -version = "0.58.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-implement 0.59.0", + "windows-interface", + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.2", ] [[package]] name = "windows-core" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.0", - "windows-result 0.3.0", - "windows-strings 0.3.0", - "windows-targets 0.53.0", + "windows-implement 0.60.0", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.2", ] [[package]] -name = "windows-implement" -version = "0.58.0" +name = "windows-future" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", + "windows-core 0.61.2", + "windows-link", + "windows-threading", ] [[package]] @@ -5666,77 +5308,72 @@ checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] -name = "windows-interface" -version = "0.58.0" +name = "windows-implement" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "windows-interface" -version = "0.59.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] -name = "windows-registry" -version = "0.4.0" +name = "windows-link" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result 0.3.0", - "windows-strings 0.3.0", - "windows-targets 0.53.0", -] +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "windows-result" +name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-targets 0.52.6", + "windows-core 0.61.2", + "windows-link", ] [[package]] name = "windows-result" -version = "0.3.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.53.0", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-targets 0.53.0", + "windows-link", ] [[package]] @@ -5775,6 +5412,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -5823,9 +5469,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -5837,6 +5483,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6019,9 +5674,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.2" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] @@ -6042,7 +5697,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", ] [[package]] @@ -6055,28 +5710,22 @@ dependencies = [ "futures", "log", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "windows 0.59.0", "windows-core 0.59.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "ws_stream_wasm" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" dependencies = [ "async_io_stream", "futures", @@ -6085,7 +5734,7 @@ dependencies = [ "pharos", "rustc_version", "send_wrapper", - "thiserror 1.0.69", + "thiserror 2.0.12", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6110,9 +5759,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "xmltree" @@ -6134,9 +5783,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -6146,13 +5795,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "synstructure", ] @@ -6164,63 +5813,42 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" [[package]] name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.18" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79386d31a42a4996e3336b0919ddb90f81112af416270cff95b5f5af22b839c2" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive 0.8.18", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.18" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76331675d372f91bf8d17e13afbd5fe639200b73d01f0fc748bb059f9cca2db7" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "synstructure", ] @@ -6230,11 +5858,22 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -6243,11 +5882,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] diff --git a/Cargo.toml b/Cargo.toml index 27b47f1..2e8d3f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,109 +11,89 @@ repository = "https://github.com/n0-computer/iroh-docs" # Sadly this also needs to be updated in .github/workflows/ci.yml rust-version = "1.81" -[lints.rust] -missing_debug_implementations = "warn" - -# We use this --cfg for documenting the cargo features on which an API -# is available. To preview this locally use: RUSTFLAGS="--cfg -# iroh_docsrs cargo +nightly doc --all-features". We use our own -# iroh_docsrs instead of the common docsrs to avoid also enabling this -# feature in any dependencies, because some indirect dependencies -# require a feature enabled when using `--cfg docsrs` which we can not -# do. To enable for a crate set `#![cfg_attr(iroh_docsrs, -# feature(doc_cfg))]` in the crate. -unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)"] } - [dependencies] anyhow = "1" async-channel = "2.3.1" -blake3 = { package = "iroh-blake3", version = "1.4.5"} +blake3 = "1.8" bytes = { version = "1.7", features = ["serde"] } -derive_more = { version = "1.0.0", features = ["debug", "deref", "display", "from", "try_into", "into", "as_ref"] } +derive_more = { version = "1.0.0", features = [ + "debug", + "deref", + "display", + "from", + "try_into", + "into", + "as_ref", +] } ed25519-dalek = { version = "2.0.0", features = ["serde", "rand_core"] } futures-buffered = "0.2.4" futures-lite = "2.3.0" futures-util = { version = "0.3.25" } hex = "0.4" -iroh-base = { version = "0.35", features = ["ticket"] } -iroh-blobs = { version = "0.35" } -iroh-gossip = { version = "0.35", optional = true, features = ["net"] } -iroh-metrics = { version = "0.34", default-features = false } -iroh = { version = "0.35", optional = true } +iroh = { version = "0.90" } +iroh-base = { version = "0.90", features = ["ticket"] } +iroh-blobs = { version = "0.90" } +iroh-gossip = { version = "0.90", features = ["net"] } +iroh-metrics = { version = "0.35", default-features = false } +irpc = { version = "0.5.0" } +n0-future = "0.1.3" num_enum = "0.7" -postcard = { version = "1", default-features = false, features = ["alloc", "use-std", "experimental-derive"] } +postcard = { version = "1", default-features = false, features = [ + "alloc", + "use-std", + "experimental-derive", +] } +quinn = { package = "iroh-quinn", version = "0.14.0" } rand = "0.8.5" rand_core = "0.6.4" redb = { version = "2.0.0" } -redb_v1 = { package = "redb", version = "1.5.1" } +redb_v1 = { package = "redb", version = "1.5.1" } self_cell = "1.0.3" serde = { version = "1.0.164", features = ["derive"] } +serde-error = "0.1.3" strum = { version = "0.26", features = ["derive"] } tempfile = { version = "3.4" } thiserror = "2" -tokio = { version = "1", features = ["sync", "rt", "time", "macros"] } -tokio-stream = { version = "0.1", optional = true, features = ["sync"]} -tokio-util = { version = "0.7.12", optional = true, features = ["codec", "io-util", "io", "rt"] } +tokio = { version = "1", features = ["sync", "rt", "time", "io-util"] } +tokio-stream = { version = "0.1", features = ["sync"] } +tokio-util = { version = "0.7.12", features = ["codec", "io-util", "io", "rt"] } tracing = "0.1" -# rpc -nested_enum_utils = { version = "0.1.0", optional = true } -quic-rpc = { version = "0.20", optional = true } -quic-rpc-derive = { version = "0.20", optional = true } -serde-error = { version = "0.1.3", optional = true } -portable-atomic = { version = "1.9.0", optional = true } - -# cli -clap = { version = "4", features = ["derive"], optional = true } -console = { version = "0.15", optional = true } -data-encoding = { version = "2.3.3", optional = true } -indicatif = { version = "0.17", features = ["tokio"], optional = true } -dialoguer = { version = "0.11", optional = true } -colored = { version = "2.1", optional = true } -shellexpand = { version = "3.1", optional = true } - [dev-dependencies] -rand_chacha = "0.3.1" -tokio = { version = "1", features = ["sync", "macros"] } +data-encoding = "2.6.0" +iroh = { version = "0.90", features = ["test-utils"] } +nested_enum_utils = "0.1.0" +parking_lot = "0.12.3" proptest = "1.2.0" +rand_chacha = "0.3.1" tempfile = "3.4" -tracing-test = "0.2.5" test-strategy = "0.4" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -parking_lot = "0.12.3" -testresult = "0.4.1" -nested_enum_utils = "0.1.0" -iroh-io = "0.6.1" testdir = "0.7" -data-encoding = "2.6.0" +testresult = "0.4.1" +tokio = { version = "1", features = ["sync", "macros"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tracing-test = "0.2.5" [features] -default = ["net", "metrics", "engine", "test-utils"] -net = ["dep:iroh", "tokio/io-util", "dep:tokio-stream", "dep:tokio-util"] +default = ["metrics"] metrics = ["iroh-metrics/metrics", "iroh/metrics"] -engine = ["net", "dep:iroh-gossip", "iroh-blobs/downloader"] -test-utils = ["iroh/test-utils"] -cli = [ - "rpc", - "dep:clap", - "dep:indicatif", - "dep:console", - "dep:colored", - "dep:dialoguer", - "dep:shellexpand", - "dep:data-encoding", - "iroh-blobs/rpc", -] -rpc = [ - "engine", - "dep:nested_enum_utils", - "dep:quic-rpc", - "dep:quic-rpc-derive", - "dep:serde-error", - "dep:portable-atomic", - "iroh-blobs/rpc", -] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "iroh_docsrs"] + +[lints.rust] +missing_debug_implementations = "warn" + +# We use this --cfg for documenting the cargo features on which an API +# is available. To preview this locally use: RUSTFLAGS="--cfg +# iroh_docsrs cargo +nightly doc --all-features". We use our own +# iroh_docsrs instead of the common docsrs to avoid also enabling this +# feature in any dependencies, because some indirect dependencies +# require a feature enabled when using `--cfg docsrs` which we can not +# do. To enable for a crate set `#![cfg_attr(iroh_docsrs, +# feature(doc_cfg))]` in the crate. +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)"] } + +[patch.crates-io] +iroh-blobs = { git = "https://github.com/n0-computer/iroh-blobs", branch = "Frando/gc-protect" } diff --git a/README.md b/README.md index 336afff..65fa00f 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Here is a basic example of how to set up `iroh-docs` with `iroh`: ```rust use iroh::{protocol::Router, Endpoint}; -use iroh_blobs::{net_protocol::Blobs, util::local_pool::LocalPool, ALPN as BLOBS_ALPN}; +use iroh_blobs::{net_protocol::Blobs, store::mem::MemStore, ALPN as BLOBS_ALPN}; use iroh_docs::{protocol::Docs, ALPN as DOCS_ALPN}; use iroh_gossip::{net::Gossip, ALPN as GOSSIP_ALPN}; @@ -53,23 +53,25 @@ async fn main() -> anyhow::Result<()> { // we've built at number0 let endpoint = Endpoint::builder().discovery_n0().bind().await?; - // create a router builder, we will add the - // protocols to this builder and then spawn - // the router - let builder = Router::builder(endpoint); - // build the blobs protocol - let blobs = Blobs::memory().build(builder.endpoint()); + let blobs = MemStore::default(); // build the gossip protocol - let gossip = Gossip::builder().spawn(builder.endpoint().clone()).await?; + let gossip = Gossip::builder().spawn(endpoint.clone()); // build the docs protocol - let docs = Docs::memory().spawn(&blobs, &gossip).await?; + let docs = Docs::memory() + .spawn(endpoint.clone(), (*blobs).clone(), gossip.clone()) + .await?; + + // create a router builder, we will add the + // protocols to this builder and then spawn + // the router + let builder = Router::builder(endpoint.clone()); // setup router - let router = builder - .accept(BLOBS_ALPN, blobs) + let _router = builder + .accept(BLOBS_ALPN, Blobs::new(&blobs, endpoint.clone(), None)) .accept(GOSSIP_ALPN, gossip) .accept(DOCS_ALPN, docs) .spawn(); diff --git a/examples/setup.rs b/examples/setup.rs new file mode 100644 index 0000000..e1ea79a --- /dev/null +++ b/examples/setup.rs @@ -0,0 +1,37 @@ +use iroh::{protocol::Router, Endpoint}; +use iroh_blobs::{net_protocol::Blobs, store::mem::MemStore, ALPN as BLOBS_ALPN}; +use iroh_docs::{protocol::Docs, ALPN as DOCS_ALPN}; +use iroh_gossip::{net::Gossip, ALPN as GOSSIP_ALPN}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // create an iroh endpoint that includes the standard discovery mechanisms + // we've built at number0 + let endpoint = Endpoint::builder().discovery_n0().bind().await?; + + // build the blobs protocol + let blobs = MemStore::default(); + + // build the gossip protocol + let gossip = Gossip::builder().spawn(endpoint.clone()); + + // build the docs protocol + let docs = Docs::memory() + .spawn(endpoint.clone(), (*blobs).clone(), gossip.clone()) + .await?; + + // create a router builder, we will add the + // protocols to this builder and then spawn + // the router + let builder = Router::builder(endpoint.clone()); + + // setup router + let _router = builder + .accept(BLOBS_ALPN, Blobs::new(&blobs, endpoint.clone(), None)) + .accept(GOSSIP_ALPN, gossip) + .accept(DOCS_ALPN, docs) + .spawn(); + + // do fun stuff with docs! + Ok(()) +} diff --git a/src/actor.rs b/src/actor.rs index 195cbab..00b8e35 100644 --- a/src/actor.rs +++ b/src/actor.rs @@ -12,20 +12,24 @@ use anyhow::{anyhow, Context, Result}; use bytes::Bytes; use futures_util::FutureExt; use iroh_blobs::Hash; +use irpc::channel::mpsc; use serde::{Deserialize, Serialize}; use tokio::{sync::oneshot, task::JoinSet}; use tracing::{debug, error, error_span, trace, warn}; use crate::{ + api::{ + protocol::{AuthorListResponse, ListResponse}, + RpcError, RpcResult, + }, metrics::Metrics, ranger::Message, store::{ fs::{ContentHashesIterator, StoreInstance}, DownloadPolicy, ImportNamespaceOutcome, Query, Store, }, - Author, AuthorHeads, AuthorId, Capability, CapabilityKind, ContentStatus, - ContentStatusCallback, Event, NamespaceId, NamespaceSecret, PeerIdBytes, Replica, ReplicaInfo, - SignedEntry, SyncOutcome, + Author, AuthorHeads, AuthorId, Capability, ContentStatus, ContentStatusCallback, Event, + NamespaceId, NamespaceSecret, PeerIdBytes, Replica, ReplicaInfo, SignedEntry, SyncOutcome, }; const ACTION_CAP: usize = 1024; @@ -60,12 +64,12 @@ enum Action { #[display("ListAuthors")] ListAuthors { #[debug("reply")] - reply: async_channel::Sender>, + reply: mpsc::Sender>, }, #[display("ListReplicas")] ListReplicas { #[debug("reply")] - reply: async_channel::Sender>, + reply: mpsc::Sender>, }, #[display("ContentHashes")] ContentHashes { @@ -165,7 +169,7 @@ enum ReplicaAction { }, GetMany { query: Query, - reply: async_channel::Sender>, + reply: mpsc::Sender>, }, DropReplica { reply: oneshot::Sender>, @@ -290,6 +294,7 @@ impl SyncHandle { } pub async fn open(&self, namespace: NamespaceId, opts: OpenOpts) -> Result<()> { + tracing::debug!("SyncHandle::open called"); let (reply, rx) = oneshot::channel(); let action = ReplicaAction::Open { reply, opts }; self.send_replica(namespace, action).await?; @@ -443,7 +448,7 @@ impl SyncHandle { &self, namespace: NamespaceId, query: Query, - reply: async_channel::Sender>, + reply: mpsc::Sender>, ) -> Result<()> { let action = ReplicaAction::GetMany { query, reply }; self.send_replica(namespace, action).await?; @@ -497,14 +502,14 @@ impl SyncHandle { Ok(store) } - pub async fn list_authors(&self, reply: async_channel::Sender>) -> Result<()> { + pub async fn list_authors( + &self, + reply: mpsc::Sender>, + ) -> Result<()> { self.send(Action::ListAuthors { reply }).await } - pub async fn list_replicas( - &self, - reply: async_channel::Sender>, - ) -> Result<()> { + pub async fn list_replicas(&self, reply: mpsc::Sender>) -> Result<()> { self.send(Action::ListReplicas { reply }).await } @@ -649,7 +654,7 @@ impl Actor { break reply; } action => { - if self.on_action(action).is_err() { + if self.on_action(action).await.is_err() { warn!("failed to send reply: receiver dropped"); } } @@ -667,7 +672,7 @@ impl Actor { } } - fn on_action(&mut self, action: Action) -> Result<(), SendReplyError> { + async fn on_action(&mut self, action: Action) -> Result<(), SendReplyError> { match action { Action::Shutdown { .. } => { unreachable!("Shutdown is handled in run()") @@ -696,26 +701,29 @@ impl Actor { let iter = self .store .list_authors() - .map(|a| a.map(|a| a.map(|a| a.id()))); + .map(|a| a.map(|a| a.map(|a| AuthorListResponse { author_id: a.id() }))); self.tasks - .spawn_local(iter_to_channel_async(reply, iter).map(|_| ())); + .spawn_local(iter_to_irpc(reply, iter).map(|_| ())); Ok(()) } Action::ListReplicas { reply } => { let iter = self.store.list_namespaces(); + let iter = iter.map(|inner| { + inner.map(|res| res.map(|(id, capability)| ListResponse { id, capability })) + }); self.tasks - .spawn_local(iter_to_channel_async(reply, iter).map(|_| ())); + .spawn_local(iter_to_irpc(reply, iter).map(|_| ())); Ok(()) } Action::ContentHashes { reply } => { send_reply_with(reply, self, |this| this.store.content_hashes()) } Action::FlushStore { reply } => send_reply(reply, self.store.flush()), - Action::Replica(namespace, action) => self.on_replica_action(namespace, action), + Action::Replica(namespace, action) => self.on_replica_action(namespace, action).await, } } - fn on_replica_action( + async fn on_replica_action( &mut self, namespace: NamespaceId, action: ReplicaAction, @@ -801,13 +809,19 @@ impl Actor { from, mut state, reply, - } => send_reply_with(reply, self, move |this| { - let mut replica = this - .states - .replica_if_syncing(&namespace, &mut this.store)?; - let res = replica.sync_process_message(message, from, &mut state)?; - Ok((res, state)) - }), + } => { + let res = async { + let mut replica = self + .states + .replica_if_syncing(&namespace, &mut self.store)?; + let res = replica + .sync_process_message(message, from, &mut state) + .await?; + Ok((res, state)) + } + .await; + reply.send(res).map_err(send_reply_error) + } ReplicaAction::GetSyncPeers { reply } => send_reply_with(reply, self, move |this| { this.states.ensure_open(&namespace)?; let peers = this.store.get_sync_peers(&namespace)?; @@ -832,7 +846,7 @@ impl Actor { .ensure_open(&namespace) .and_then(|_| self.store.get_many(namespace, query)); self.tasks - .spawn_local(iter_to_channel_async(reply, iter).map(|_| ())); + .spawn_local(iter_to_irpc(reply, iter).map(|_| ())); Ok(()) } ReplicaAction::DropReplica { reply } => send_reply_with(reply, self, |this| { @@ -978,6 +992,7 @@ impl OpenReplicas { } hash_map::Entry::Occupied(mut e) => { let state = e.get_mut(); + tracing::debug!("STATE {state:?}"); state.handles = state.handles.wrapping_sub(1); if state.handles == 0 { let _ = e.remove_entry(); @@ -995,14 +1010,18 @@ impl OpenReplicas { } } -async fn iter_to_channel_async( - channel: async_channel::Sender>, +async fn iter_to_irpc( + channel: mpsc::Sender>, iter: Result>>, ) -> Result<(), SendReplyError> { match iter { - Err(err) => channel.send(Err(err)).await.map_err(send_reply_error)?, + Err(err) => channel + .send(Err(RpcError::new(&*err))) + .await + .map_err(send_reply_error)?, Ok(iter) => { for item in iter { + let item = item.map_err(|err| RpcError::new(&*err)); channel.send(item).await.map_err(send_reply_error)?; } } diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..0868e80 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,691 @@ +//! irpc-based RPC implementation for docs. + +#![allow(missing_docs)] + +use std::{ + future::Future, + net::SocketAddr, + path::Path, + pin::Pin, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + task::{ready, Poll}, +}; + +use anyhow::{Context, Result}; +use bytes::Bytes; +use iroh::NodeAddr; +use iroh_blobs::{ + api::blobs::{AddPathOptions, AddProgressItem, ExportMode, ExportOptions, ExportProgress}, + Hash, +}; +use irpc::rpc::Handler; +use n0_future::{ + task::{self, AbortOnDropHandle}, + FutureExt, Stream, StreamExt, +}; + +use self::{ + actor::RpcActor, + protocol::{ + AddrInfoOptions, AuthorCreateRequest, AuthorDeleteRequest, AuthorExportRequest, + AuthorGetDefaultRequest, AuthorImportRequest, AuthorListRequest, AuthorSetDefaultRequest, + CloseRequest, CreateRequest, DelRequest, DocsMessage, DocsProtocol, DocsService, + DropRequest, GetDownloadPolicyRequest, GetExactRequest, GetManyRequest, + GetSyncPeersRequest, ImportRequest, LeaveRequest, ListRequest, OpenRequest, + SetDownloadPolicyRequest, SetHashRequest, SetRequest, ShareMode, ShareRequest, + StartSyncRequest, StatusRequest, SubscribeRequest, + }, +}; +use crate::{ + actor::OpenState, + engine::{Engine, LiveEvent}, + store::{DownloadPolicy, Query}, + Author, AuthorId, Capability, CapabilityKind, DocTicket, Entry, NamespaceId, PeerIdBytes, +}; + +pub(crate) mod actor; +pub mod protocol; + +pub type RpcError = serde_error::Error; +pub type RpcResult = std::result::Result; + +type Client = irpc::Client; + +/// API wrapper for the docs service +#[derive(Debug, Clone)] +pub struct DocsApi { + pub(crate) inner: Client, +} + +impl DocsApi { + /// Create a new docs API from an engine + pub fn spawn(engine: Arc) -> Self { + RpcActor::spawn(engine) + } + + /// Connect to a remote docs service + pub fn connect(endpoint: quinn::Endpoint, addr: SocketAddr) -> Result { + Ok(DocsApi { + inner: Client::quinn(endpoint, addr), + }) + } + + /// Listen for incoming RPC connections + pub fn listen(&self, endpoint: quinn::Endpoint) -> Result> { + let local = self.inner.local().context("cannot listen on remote API")?; + let handler: Handler = Arc::new(move |msg, _rx, tx| { + let local = local.clone(); + Box::pin(match msg { + DocsProtocol::Open(msg) => local.send((msg, tx)), + DocsProtocol::Close(msg) => local.send((msg, tx)), + DocsProtocol::Status(msg) => local.send((msg, tx)), + DocsProtocol::List(msg) => local.send((msg, tx)), + DocsProtocol::Create(msg) => local.send((msg, tx)), + DocsProtocol::Drop(msg) => local.send((msg, tx)), + DocsProtocol::Import(msg) => local.send((msg, tx)), + DocsProtocol::Set(msg) => local.send((msg, tx)), + DocsProtocol::SetHash(msg) => local.send((msg, tx)), + DocsProtocol::Get(msg) => local.send((msg, tx)), + DocsProtocol::GetExact(msg) => local.send((msg, tx)), + // DocsProtocol::ImportFile(msg) => local.send((msg, tx)), + // DocsProtocol::ExportFile(msg) => local.send((msg, tx)), + DocsProtocol::Del(msg) => local.send((msg, tx)), + DocsProtocol::StartSync(msg) => local.send((msg, tx)), + DocsProtocol::Leave(msg) => local.send((msg, tx)), + DocsProtocol::Share(msg) => local.send((msg, tx)), + DocsProtocol::Subscribe(msg) => local.send((msg, tx)), + DocsProtocol::GetDownloadPolicy(msg) => local.send((msg, tx)), + DocsProtocol::SetDownloadPolicy(msg) => local.send((msg, tx)), + DocsProtocol::GetSyncPeers(msg) => local.send((msg, tx)), + DocsProtocol::AuthorList(msg) => local.send((msg, tx)), + DocsProtocol::AuthorCreate(msg) => local.send((msg, tx)), + DocsProtocol::AuthorGetDefault(msg) => local.send((msg, tx)), + DocsProtocol::AuthorSetDefault(msg) => local.send((msg, tx)), + DocsProtocol::AuthorImport(msg) => local.send((msg, tx)), + DocsProtocol::AuthorExport(msg) => local.send((msg, tx)), + DocsProtocol::AuthorDelete(msg) => local.send((msg, tx)), + }) + }); + let join_handle = task::spawn(irpc::rpc::listen(endpoint, handler)); + Ok(AbortOnDropHandle::new(join_handle)) + } + + /// Creates a new document author. + /// + /// You likely want to save the returned [`AuthorId`] somewhere so that you can use this author + /// again. + /// + /// If you need only a single author, use [`Self::author_default`]. + pub async fn author_create(&self) -> Result { + let response = self.inner.rpc(AuthorCreateRequest).await??; + Ok(response.author_id) + } + + /// Returns the default document author of this node. + /// + /// On persistent nodes, the author is created on first start and its public key is saved + /// in the data directory. + /// + /// The default author can be set with [`Self::author_set_default`]. + pub async fn author_default(&self) -> Result { + let response = self.inner.rpc(AuthorGetDefaultRequest).await??; + Ok(response.author_id) + } + + /// Sets the node-wide default author. + /// + /// If the author does not exist, an error is returned. + /// + /// On a persistent node, the author id will be saved to a file in the data directory and + /// reloaded after a restart. + pub async fn author_set_default(&self, author_id: AuthorId) -> Result<()> { + self.inner + .rpc(AuthorSetDefaultRequest { author_id }) + .await??; + Ok(()) + } + + /// Lists document authors for which we have a secret key. + /// + /// It's only possible to create writes from authors that we have the secret key of. + pub async fn author_list(&self) -> Result>> { + let stream = self.inner.server_streaming(AuthorListRequest, 64).await?; + Ok(stream.into_stream().map(|res| match res { + Err(err) => Err(err.into()), + Ok(Err(err)) => Err(err.into()), + Ok(Ok(res)) => Ok(res.author_id), + })) + } + + /// Exports the given author. + /// + /// Warning: The [`Author`] struct contains sensitive data. + pub async fn author_export(&self, author: AuthorId) -> Result> { + let response = self.inner.rpc(AuthorExportRequest { author }).await??; + Ok(response.author) + } + + /// Imports the given author. + /// + /// Warning: The [`Author`] struct contains sensitive data. + pub async fn author_import(&self, author: Author) -> Result<()> { + self.inner.rpc(AuthorImportRequest { author }).await??; + Ok(()) + } + + /// Deletes the given author by id. + /// + /// Warning: This permanently removes this author. + /// + /// Returns an error if attempting to delete the default author. + pub async fn author_delete(&self, author: AuthorId) -> Result<()> { + self.inner.rpc(AuthorDeleteRequest { author }).await??; + Ok(()) + } + + /// Creates a new document. + pub async fn create(&self) -> Result { + let response = self.inner.rpc(CreateRequest).await??; + Ok(Doc::new(self.inner.clone(), response.id)) + } + + /// Deletes a document from the local node. + /// + /// This is a destructive operation. Both the document secret key and all entries in the + /// document will be permanently deleted from the node's storage. Content blobs will be deleted + /// through garbage collection unless they are referenced from another document or tag. + pub async fn drop_doc(&self, doc_id: NamespaceId) -> Result<()> { + self.inner.rpc(DropRequest { doc_id }).await??; + Ok(()) + } + + /// Imports a document from a namespace capability. + /// + /// This does not start sync automatically. Use [`Doc::start_sync`] to start sync. + pub async fn import_namespace(&self, capability: Capability) -> Result { + let response = self.inner.rpc(ImportRequest { capability }).await??; + Ok(Doc::new(self.inner.clone(), response.doc_id)) + } + + /// Imports a document from a ticket and joins all peers in the ticket. + pub async fn import(&self, ticket: DocTicket) -> Result { + let DocTicket { capability, nodes } = ticket; + let doc = self.import_namespace(capability).await?; + doc.start_sync(nodes).await?; + Ok(doc) + } + + /// Imports a document from a ticket, creates a subscription stream and joins all peers in the ticket. + /// + /// Returns the [`Doc`] and a [`Stream`] of [`LiveEvent`]s. + /// + /// The subscription stream is created before the sync is started, so the first call to this + /// method after starting the node is guaranteed to not miss any sync events. + pub async fn import_and_subscribe( + &self, + ticket: DocTicket, + ) -> Result<(Doc, impl Stream>)> { + let DocTicket { capability, nodes } = ticket; + let response = self.inner.rpc(ImportRequest { capability }).await??; + let doc = Doc::new(self.inner.clone(), response.doc_id); + let events = doc.subscribe().await?; + doc.start_sync(nodes).await?; + Ok((doc, events)) + } + + /// Lists all documents. + pub async fn list( + &self, + ) -> Result> + Unpin + Send + 'static> + { + let stream = self.inner.server_streaming(ListRequest, 64).await?; + let stream = Box::pin(stream.into_stream()); + Ok(stream.map(|res| match res { + Err(err) => Err(err.into()), + Ok(Err(err)) => Err(err.into()), + Ok(Ok(res)) => Ok((res.id, res.capability)), + })) + } + + /// Returns a [`Doc`] client for a single document. + /// + /// Returns None if the document cannot be found. + pub async fn open(&self, id: NamespaceId) -> Result> { + self.inner.rpc(OpenRequest { doc_id: id }).await??; + Ok(Some(Doc::new(self.inner.clone(), id))) + } +} + +/// Document handle +#[derive(Debug, Clone)] +pub struct Doc { + inner: Client, + namespace_id: NamespaceId, + closed: Arc, +} + +impl Doc { + fn new(inner: Client, namespace_id: NamespaceId) -> Self { + Self { + inner, + namespace_id, + closed: Default::default(), + } + } + + /// Returns the document id of this doc. + pub fn id(&self) -> NamespaceId { + self.namespace_id + } + + /// Closes the document. + pub async fn close(&self) -> Result<()> { + self.closed.store(true, Ordering::Relaxed); + self.inner + .rpc(CloseRequest { + doc_id: self.namespace_id, + }) + .await??; + Ok(()) + } + + fn ensure_open(&self) -> Result<()> { + if self.closed.load(Ordering::Relaxed) { + Err(anyhow::anyhow!("document is closed")) + } else { + Ok(()) + } + } + + /// Sets the content of a key to a byte array. + pub async fn set_bytes( + &self, + author_id: AuthorId, + key: impl Into, + value: impl Into, + ) -> Result { + self.ensure_open()?; + let response = self + .inner + .rpc(SetRequest { + doc_id: self.namespace_id, + author_id, + key: key.into(), + value: value.into(), + }) + .await??; + Ok(response.entry.content_hash()) + } + + /// Sets an entry on the doc via its key, hash, and size. + pub async fn set_hash( + &self, + author_id: AuthorId, + key: impl Into, + hash: Hash, + size: u64, + ) -> Result<()> { + self.ensure_open()?; + self.inner + .rpc(SetHashRequest { + doc_id: self.namespace_id, + author_id, + key: key.into(), + hash, + size, + }) + .await??; + Ok(()) + } + + /// Deletes entries that match the given `author` and key `prefix`. + /// + /// This inserts an empty entry with the key set to `prefix`, effectively clearing all other + /// entries whose key starts with or is equal to the given `prefix`. + /// + /// Returns the number of entries deleted. + pub async fn del(&self, author_id: AuthorId, prefix: impl Into) -> Result { + self.ensure_open()?; + let response = self + .inner + .rpc(DelRequest { + doc_id: self.namespace_id, + author_id, + prefix: prefix.into(), + }) + .await??; + Ok(response.removed) + } + + /// Returns an entry for a key and author. + /// + /// Optionally also returns the entry unless it is empty (i.e. a deletion marker). + pub async fn get_exact( + &self, + author: AuthorId, + key: impl AsRef<[u8]>, + include_empty: bool, + ) -> Result> { + self.ensure_open()?; + let response = self + .inner + .rpc(GetExactRequest { + author, + key: key.as_ref().to_vec().into(), + doc_id: self.namespace_id, + include_empty, + }) + .await??; + Ok(response.entry.map(|entry| entry.into())) + } + + /// Returns all entries matching the query. + pub async fn get_many( + &self, + query: impl Into, + ) -> Result>> { + self.ensure_open()?; + let stream = self + .inner + .server_streaming( + GetManyRequest { + doc_id: self.namespace_id, + query: query.into(), + }, + 64, + ) + .await?; + Ok(stream.into_stream().map(|res| match res { + Err(err) => Err(err.into()), + Ok(Err(err)) => Err(err.into()), + Ok(Ok(res)) => Ok(res.into()), + })) + } + + /// Returns a single entry. + pub async fn get_one(&self, query: impl Into) -> Result> { + self.ensure_open()?; + let stream = self.get_many(query).await?; + tokio::pin!(stream); + futures_lite::StreamExt::next(&mut stream).await.transpose() + } + + /// Shares this document with peers over a ticket. + pub async fn share(&self, mode: ShareMode, addr_options: AddrInfoOptions) -> Result { + self.ensure_open()?; + let response = self + .inner + .rpc(ShareRequest { + doc_id: self.namespace_id, + mode, + addr_options, + }) + .await??; + Ok(response.0) + } + + /// Starts to sync this document with a list of peers. + pub async fn start_sync(&self, peers: Vec) -> Result<()> { + self.ensure_open()?; + self.inner + .rpc(StartSyncRequest { + doc_id: self.namespace_id, + peers, + }) + .await??; + Ok(()) + } + + /// Stops the live sync for this document. + pub async fn leave(&self) -> Result<()> { + self.ensure_open()?; + self.inner + .rpc(LeaveRequest { + doc_id: self.namespace_id, + }) + .await??; + Ok(()) + } + + /// Subscribes to events for this document. + pub async fn subscribe( + &self, + ) -> Result> + Send + Unpin + 'static> { + self.ensure_open()?; + let stream = self + .inner + .server_streaming( + SubscribeRequest { + doc_id: self.namespace_id, + }, + 64, + ) + .await?; + Ok(Box::pin(stream.into_stream().map(|res| match res { + Err(err) => Err(err.into()), + Ok(Err(err)) => Err(err.into()), + Ok(Ok(res)) => Ok(res.event), + }))) + } + + /// Returns status info for this document + pub async fn status(&self) -> Result { + self.ensure_open()?; + let response = self + .inner + .rpc(StatusRequest { + doc_id: self.namespace_id, + }) + .await??; + Ok(response.status) + } + + /// Sets the download policy for this document + pub async fn set_download_policy(&self, policy: DownloadPolicy) -> Result<()> { + self.ensure_open()?; + self.inner + .rpc(SetDownloadPolicyRequest { + doc_id: self.namespace_id, + policy, + }) + .await??; + Ok(()) + } + + /// Returns the download policy for this document + pub async fn get_download_policy(&self) -> Result { + self.ensure_open()?; + let response = self + .inner + .rpc(GetDownloadPolicyRequest { + doc_id: self.namespace_id, + }) + .await??; + Ok(response.policy) + } + + /// Returns sync peers for this document + pub async fn get_sync_peers(&self) -> Result>> { + self.ensure_open()?; + let response = self + .inner + .rpc(GetSyncPeersRequest { + doc_id: self.namespace_id, + }) + .await??; + Ok(response.peers) + } + + /// Adds an entry from an absolute file path + pub async fn import_file( + &self, + blobs: &iroh_blobs::api::Store, + author: AuthorId, + key: Bytes, + path: impl AsRef, + import_mode: iroh_blobs::api::blobs::ImportMode, + ) -> Result { + self.ensure_open()?; + let progress = blobs.add_path_with_opts(AddPathOptions { + path: path.as_ref().to_owned(), + format: iroh_blobs::BlobFormat::Raw, + mode: import_mode, + }); + let stream = progress.stream().await; + let doc = self.clone(); + let ctx = EntryContext { + doc, + author, + key, + size: None, + }; + Ok(ImportFileProgress(ImportInner::Blobs( + Box::pin(stream), + Some(ctx), + ))) + } + + /// Exports an entry as a file to a given absolute path. + pub async fn export_file( + &self, + blobs: &iroh_blobs::api::Store, + entry: Entry, + path: impl AsRef, + mode: ExportMode, + ) -> Result { + self.ensure_open()?; + let hash = entry.content_hash(); + let progress = blobs.export_with_opts(ExportOptions { + hash, + mode, + target: path.as_ref().to_path_buf(), + }); + Ok(progress) + } +} + +#[derive(Debug)] +pub enum ImportFileProgressItem { + Error(anyhow::Error), + Blobs(AddProgressItem), + Done(ImportFileOutcome), +} + +#[derive(Debug)] +pub struct ImportFileProgress(ImportInner); + +#[derive(derive_more::Debug)] +enum ImportInner { + #[debug("Blobs")] + Blobs( + n0_future::boxed::BoxStream, + Option, + ), + #[debug("Entry")] + Entry(n0_future::boxed::BoxFuture>), + Done, +} + +struct EntryContext { + doc: Doc, + author: AuthorId, + key: Bytes, + size: Option, +} + +impl Stream for ImportFileProgress { + type Item = ImportFileProgressItem; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + match this.0 { + ImportInner::Blobs(ref mut progress, ref mut context) => { + match ready!(progress.poll_next(cx)) { + Some(item) => match item { + AddProgressItem::Size(size) => { + context + .as_mut() + .expect("Size must be emitted before done") + .size = Some(size); + Poll::Ready(Some(ImportFileProgressItem::Blobs(AddProgressItem::Size( + size, + )))) + } + AddProgressItem::Error(err) => { + *this = Self(ImportInner::Done); + Poll::Ready(Some(ImportFileProgressItem::Error(err.into()))) + } + AddProgressItem::Done(tag) => { + let EntryContext { + doc, + author, + key, + size, + } = context + .take() + .expect("AddProgressItem::Done may be emitted only once"); + let size = size.expect("Size must be emitted before done"); + let hash = *tag.hash(); + *this = Self(ImportInner::Entry(Box::pin(async move { + doc.set_hash(author, key.clone(), hash, size).await?; + Ok(ImportFileOutcome { hash, size, key }) + }))); + Poll::Ready(Some(ImportFileProgressItem::Blobs(AddProgressItem::Done( + tag, + )))) + } + item => Poll::Ready(Some(ImportFileProgressItem::Blobs(item))), + }, + None => todo!(), + } + } + ImportInner::Entry(ref mut fut) => { + let res = ready!(fut.poll(cx)); + *this = Self(ImportInner::Done); + match res { + Ok(outcome) => Poll::Ready(Some(ImportFileProgressItem::Done(outcome))), + Err(err) => Poll::Ready(Some(ImportFileProgressItem::Error(err))), + } + } + ImportInner::Done => Poll::Ready(None), + } + } +} + +impl Future for ImportFileProgress { + type Output = Result; + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + loop { + match self.as_mut().poll_next(cx) { + Poll::Ready(Some(item)) => match item { + ImportFileProgressItem::Error(error) => return Poll::Ready(Err(error)), + ImportFileProgressItem::Blobs(_add_progress_item) => continue, + ImportFileProgressItem::Done(outcome) => return Poll::Ready(Ok(outcome)), + }, + Poll::Ready(None) => { + return Poll::Ready(Err(anyhow::anyhow!( + "ImportFileProgress polled after completion" + ))) + } + Poll::Pending => return Poll::Pending, + } + } + } +} + +/// Outcome of a [`Doc::import_file`] operation +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImportFileOutcome { + /// The hash of the entry's content + pub hash: Hash, + /// The size of the entry + pub size: u64, + /// The key of the entry + pub key: Bytes, +} diff --git a/src/api/actor.rs b/src/api/actor.rs new file mode 100644 index 0000000..c4a5b56 --- /dev/null +++ b/src/api/actor.rs @@ -0,0 +1,607 @@ +use std::sync::Arc; + +use anyhow::anyhow; +use futures_lite::StreamExt; +use iroh::Watcher; +use irpc::{channel::mpsc, LocalSender, WithChannels}; +use n0_future::task::{self}; +use tokio::sync::mpsc as tokio_mpsc; +use tracing::error; + +use super::{ + protocol::{ + AuthorCreateRequest, AuthorCreateResponse, AuthorDeleteRequest, AuthorDeleteResponse, + AuthorExportRequest, AuthorExportResponse, AuthorGetDefaultRequest, + AuthorGetDefaultResponse, AuthorImportRequest, AuthorImportResponse, AuthorListRequest, + AuthorListResponse, AuthorSetDefaultRequest, AuthorSetDefaultResponse, CloseRequest, + CloseResponse, CreateRequest, CreateResponse, DelRequest, DelResponse, DocsMessage, + DocsService, DropRequest, DropResponse, GetDownloadPolicyRequest, + GetDownloadPolicyResponse, GetExactRequest, GetExactResponse, GetManyRequest, + GetSyncPeersRequest, GetSyncPeersResponse, ImportRequest, ImportResponse, LeaveRequest, + LeaveResponse, ListRequest, ListResponse, OpenRequest, OpenResponse, + SetDownloadPolicyRequest, SetDownloadPolicyResponse, SetHashRequest, SetHashResponse, + SetRequest, SetResponse, ShareMode, ShareRequest, ShareResponse, StartSyncRequest, + StartSyncResponse, StatusRequest, StatusResponse, SubscribeRequest, SubscribeResponse, + }, + DocsApi, RpcError, RpcResult, +}; +use crate::{engine::Engine, Author, DocTicket, NamespaceSecret, SignedEntry}; + +/// The docs RPC actor that handles incoming messages +pub(crate) struct RpcActor { + pub(crate) recv: tokio::sync::mpsc::Receiver, + pub(crate) engine: Arc, +} + +impl RpcActor { + pub(crate) fn spawn(engine: Arc) -> DocsApi { + let (tx, rx) = tokio_mpsc::channel(64); + let actor = Self { recv: rx, engine }; + task::spawn(actor.run()); + let local = LocalSender::::from(tx); + DocsApi { + inner: local.into(), + } + } + + pub(crate) async fn run(mut self) { + while let Some(msg) = self.recv.recv().await { + tracing::trace!("handle rpc request: {msg:?}"); + self.handle(msg).await; + } + } + + pub(crate) async fn handle(&mut self, msg: DocsMessage) { + match msg { + DocsMessage::Open(open) => { + let WithChannels { tx, inner, .. } = open; + let result = self.doc_open(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Open response: {}", e); + } + } + DocsMessage::Close(close) => { + let WithChannels { tx, inner, .. } = close; + let result = self.doc_close(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Close response: {}", e); + } + } + DocsMessage::Status(status) => { + let WithChannels { tx, inner, .. } = status; + let result = self.doc_status(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Status response: {}", e); + } + } + DocsMessage::List(list) => { + let WithChannels { tx, inner, .. } = list; + self.doc_list(inner, tx).await; + } + DocsMessage::Create(create) => { + let WithChannels { tx, inner, .. } = create; + let result = self.doc_create(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Create response: {}", e); + } + } + DocsMessage::Drop(drop) => { + let WithChannels { tx, inner, .. } = drop; + let result = self.doc_drop(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Drop response: {}", e); + } + } + DocsMessage::Import(import) => { + let WithChannels { tx, inner, .. } = import; + let result = self.doc_import(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Import response: {}", e); + } + } + DocsMessage::Set(set) => { + let WithChannels { tx, inner, .. } = set; + let result = self.doc_set(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Set response: {}", e); + } + } + DocsMessage::SetHash(set_hash) => { + let WithChannels { tx, inner, .. } = set_hash; + let result = self.doc_set_hash(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send SetHash response: {}", e); + } + } + DocsMessage::Get(get) => { + let WithChannels { tx, inner, .. } = get; + self.doc_get_many(inner, tx).await; + } + DocsMessage::GetExact(get_exact) => { + let WithChannels { tx, inner, .. } = get_exact; + let result = self.doc_get_exact(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send GetExact response: {}", e); + } + } + DocsMessage::Del(del) => { + let WithChannels { tx, inner, .. } = del; + let result = self.doc_del(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Del response: {}", e); + } + } + DocsMessage::StartSync(start_sync) => { + let WithChannels { tx, inner, .. } = start_sync; + let result = self.doc_start_sync(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send StartSync response: {}", e); + } + } + DocsMessage::Leave(leave) => { + let WithChannels { tx, inner, .. } = leave; + let result = self.doc_leave(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Leave response: {}", e); + } + } + DocsMessage::Share(share) => { + let WithChannels { tx, inner, .. } = share; + let result = self.doc_share(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send Share response: {}", e); + } + } + DocsMessage::Subscribe(subscribe) => { + let WithChannels { tx, inner, .. } = subscribe; + self.doc_subscribe(inner, tx).await; + } + DocsMessage::GetDownloadPolicy(get_policy) => { + let WithChannels { tx, inner, .. } = get_policy; + let result = self.doc_get_download_policy(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send GetDownloadPolicy response: {}", e); + } + } + DocsMessage::SetDownloadPolicy(set_policy) => { + let WithChannels { tx, inner, .. } = set_policy; + let result = self.doc_set_download_policy(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send SetDownloadPolicy response: {}", e); + } + } + DocsMessage::GetSyncPeers(get_peers) => { + let WithChannels { tx, inner, .. } = get_peers; + let result = self.doc_get_sync_peers(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send GetSyncPeers response: {}", e); + } + } + DocsMessage::AuthorList(author_list) => { + let WithChannels { tx, inner, .. } = author_list; + self.author_list(inner, tx).await; + } + DocsMessage::AuthorCreate(author_create) => { + let WithChannels { tx, inner, .. } = author_create; + let result = self.author_create(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send AuthorCreate response: {}", e); + } + } + DocsMessage::AuthorGetDefault(author_get_default) => { + let WithChannels { tx, inner, .. } = author_get_default; + let result = self.author_default(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send AuthorGetDefault response: {}", e); + } + } + DocsMessage::AuthorSetDefault(author_set_default) => { + let WithChannels { tx, inner, .. } = author_set_default; + let result = self.author_set_default(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send AuthorSetDefault response: {}", e); + } + } + DocsMessage::AuthorImport(author_import) => { + let WithChannels { tx, inner, .. } = author_import; + let result = self.author_import(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send AuthorImport response: {}", e); + } + } + DocsMessage::AuthorExport(author_export) => { + let WithChannels { tx, inner, .. } = author_export; + let result = self.author_export(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send AuthorExport response: {}", e); + } + } + DocsMessage::AuthorDelete(author_delete) => { + let WithChannels { tx, inner, .. } = author_delete; + let result = self.author_delete(inner).await; + if let Err(e) = tx.send(result).await { + error!("Failed to send AuthorDelete response: {}", e); + } + } + } + } +} + +impl std::ops::Deref for RpcActor { + type Target = Engine; + + fn deref(&self) -> &Self::Target { + &self.engine + } +} + +impl RpcActor { + pub(super) async fn author_create( + &self, + _req: AuthorCreateRequest, + ) -> RpcResult { + // TODO: pass rng + let author = Author::new(&mut rand::rngs::OsRng {}); + self.sync + .import_author(author.clone()) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(AuthorCreateResponse { + author_id: author.id(), + }) + } + + pub(super) async fn author_default( + &self, + _req: AuthorGetDefaultRequest, + ) -> RpcResult { + let author_id = self.default_author.get(); + Ok(AuthorGetDefaultResponse { author_id }) + } + + pub(super) async fn author_set_default( + &self, + req: AuthorSetDefaultRequest, + ) -> RpcResult { + self.default_author + .set(req.author_id, &self.sync) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(AuthorSetDefaultResponse) + } + + pub(super) async fn author_list( + &self, + _req: AuthorListRequest, + reply: mpsc::Sender>, + ) { + if let Err(err) = self.sync.list_authors(reply.clone()).await { + reply.send(Err(RpcError::new(&*err))).await.ok(); + } + } + + pub(super) async fn author_import( + &self, + req: AuthorImportRequest, + ) -> RpcResult { + let author_id = self + .sync + .import_author(req.author) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(AuthorImportResponse { author_id }) + } + + pub(super) async fn author_export( + &self, + req: AuthorExportRequest, + ) -> RpcResult { + let author = self + .sync + .export_author(req.author) + .await + .map_err(|e| RpcError::new(&*e))?; + + Ok(AuthorExportResponse { author }) + } + + pub(super) async fn author_delete( + &self, + req: AuthorDeleteRequest, + ) -> RpcResult { + if req.author == self.default_author.get() { + return Err(RpcError::new(&*anyhow!( + "Deleting the default author is not supported" + ))); + } + self.sync + .delete_author(req.author) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(AuthorDeleteResponse) + } + + pub(super) async fn doc_create(&self, _req: CreateRequest) -> RpcResult { + let namespace = NamespaceSecret::new(&mut rand::rngs::OsRng {}); + let id = namespace.id(); + self.sync + .import_namespace(namespace.into()) + .await + .map_err(|e| RpcError::new(&*e))?; + self.sync + .open(id, Default::default()) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(CreateResponse { id }) + } + + pub(super) async fn doc_drop(&self, req: DropRequest) -> RpcResult { + let DropRequest { doc_id } = req; + self.leave(doc_id, true) + .await + .map_err(|e| RpcError::new(&*e))?; + self.sync + .drop_replica(doc_id) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(DropResponse {}) + } + + pub(super) async fn doc_list( + &self, + _req: ListRequest, + reply: irpc::channel::mpsc::Sender>, + ) { + if let Err(err) = self.sync.list_replicas(reply.clone()).await { + reply.send(Err(RpcError::new(&*err))).await.ok(); + } + } + + pub(super) async fn doc_open(&self, req: OpenRequest) -> RpcResult { + self.sync + .open(req.doc_id, Default::default()) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(OpenResponse {}) + } + + pub(super) async fn doc_close(&self, req: CloseRequest) -> RpcResult { + tracing::debug!("close req received"); + self.sync + .close(req.doc_id) + .await + .map_err(|e| RpcError::new(&*e))?; + tracing::debug!("close req handled"); + Ok(CloseResponse {}) + } + + pub(super) async fn doc_status(&self, req: StatusRequest) -> RpcResult { + let status = self + .sync + .get_state(req.doc_id) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(StatusResponse { status }) + } + + pub(super) async fn doc_share(&self, req: ShareRequest) -> RpcResult { + let ShareRequest { + doc_id, + mode, + addr_options, + } = req; + let me = self + .endpoint + .node_addr() + .initialized() + .await + .map_err(|e| RpcError::new(&e))?; + let me = addr_options.apply(&me); + + let capability = match mode { + ShareMode::Read => crate::Capability::Read(doc_id), + ShareMode::Write => { + let secret = self + .sync + .export_secret_key(doc_id) + .await + .map_err(|e| RpcError::new(&*e))?; + crate::Capability::Write(secret) + } + }; + self.start_sync(doc_id, vec![]) + .await + .map_err(|e| RpcError::new(&*e))?; + + Ok(ShareResponse(DocTicket { + capability, + nodes: vec![me], + })) + } + + pub(super) async fn doc_subscribe( + &self, + req: SubscribeRequest, + reply: irpc::channel::mpsc::Sender>, + ) { + let mut stream = match self + .subscribe(req.doc_id) + .await + .map_err(|e| RpcError::new(&*e)) + { + Ok(stream) => stream, + Err(err) => { + reply.send(Err(err)).await.ok(); + return; + } + }; + tokio::task::spawn(async move { + loop { + tokio::select! { + msg = stream.next() => { + let Some(msg) = msg else { + break; + }; + let msg = msg + .map_err(|err| RpcError::new(&*err)) + .map(|event| SubscribeResponse { event }); + if let Err(_err) = reply.send(msg).await { + break; + } + }, + _ = reply.closed() => break, + } + } + }); + } + + pub(super) async fn doc_import(&self, req: ImportRequest) -> RpcResult { + let ImportRequest { capability } = req; + let doc_id = self + .sync + .import_namespace(capability) + .await + .map_err(|e| RpcError::new(&*e))?; + self.sync + .open(doc_id, Default::default()) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(ImportResponse { doc_id }) + } + + pub(super) async fn doc_start_sync( + &self, + req: StartSyncRequest, + ) -> RpcResult { + let StartSyncRequest { doc_id, peers } = req; + self.start_sync(doc_id, peers) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(StartSyncResponse {}) + } + + pub(super) async fn doc_leave(&self, req: LeaveRequest) -> RpcResult { + let LeaveRequest { doc_id } = req; + self.leave(doc_id, false) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(LeaveResponse {}) + } + + pub(super) async fn doc_set(&self, req: SetRequest) -> RpcResult { + let blobs_store = self.blob_store(); + let SetRequest { + doc_id, + author_id, + key, + value, + } = req; + let len = value.len(); + let tag = blobs_store + .add_bytes(value) + .temp_tag() + .await + .map_err(|e| RpcError::new(&e))?; + self.sync + .insert_local(doc_id, author_id, key.clone(), *tag.hash(), len as u64) + .await + .map_err(|e| RpcError::new(&*e))?; + let entry = self + .sync + .get_exact(doc_id, author_id, key, false) + .await + .map_err(|e| RpcError::new(&*e))? + .ok_or_else(|| RpcError::new(&*anyhow!("failed to get entry after insertion")))?; + Ok(SetResponse { entry }) + } + + pub(super) async fn doc_del(&self, req: DelRequest) -> RpcResult { + let DelRequest { + doc_id, + author_id, + prefix, + } = req; + let removed = self + .sync + .delete_prefix(doc_id, author_id, prefix) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(DelResponse { removed }) + } + + pub(super) async fn doc_set_hash(&self, req: SetHashRequest) -> RpcResult { + let SetHashRequest { + doc_id, + author_id, + key, + hash, + size, + } = req; + self.sync + .insert_local(doc_id, author_id, key.clone(), hash, size) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(SetHashResponse {}) + } + + pub(super) async fn doc_get_many( + &self, + req: GetManyRequest, + reply: irpc::channel::mpsc::Sender>, + ) { + let GetManyRequest { doc_id, query } = req; + if let Err(err) = self.sync.get_many(doc_id, query, reply.clone()).await { + reply.send(Err(RpcError::new(&*err))).await.ok(); + } + } + + pub(super) async fn doc_get_exact(&self, req: GetExactRequest) -> RpcResult { + let GetExactRequest { + doc_id, + author, + key, + include_empty, + } = req; + let entry = self + .sync + .get_exact(doc_id, author, key, include_empty) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(GetExactResponse { entry }) + } + + pub(super) async fn doc_set_download_policy( + &self, + req: SetDownloadPolicyRequest, + ) -> RpcResult { + self.sync + .set_download_policy(req.doc_id, req.policy) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(SetDownloadPolicyResponse {}) + } + + pub(super) async fn doc_get_download_policy( + &self, + req: GetDownloadPolicyRequest, + ) -> RpcResult { + let policy = self + .sync + .get_download_policy(req.doc_id) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(GetDownloadPolicyResponse { policy }) + } + + pub(super) async fn doc_get_sync_peers( + &self, + req: GetSyncPeersRequest, + ) -> RpcResult { + let peers = self + .sync + .get_sync_peers(req.doc_id) + .await + .map_err(|e| RpcError::new(&*e))?; + Ok(GetSyncPeersResponse { peers }) + } +} diff --git a/src/api/protocol.rs b/src/api/protocol.rs new file mode 100644 index 0000000..4688e20 --- /dev/null +++ b/src/api/protocol.rs @@ -0,0 +1,434 @@ +//! Protocol definitions for irpc-based RPC. + +use std::path::PathBuf; + +use bytes::Bytes; +use iroh::NodeAddr; +use iroh_blobs::{api::blobs::ExportMode, Hash}; +use irpc::{ + channel::{mpsc, oneshot}, + rpc_requests, Service, +}; +use serde::{Deserialize, Serialize}; + +use super::RpcResult; +use crate::{ + actor::OpenState, + engine::LiveEvent, + store::{DownloadPolicy, Query}, + Author, AuthorId, Capability, CapabilityKind, DocTicket, Entry, NamespaceId, PeerIdBytes, + SignedEntry, +}; + +/// The RPC service type for the docs protocol. +#[derive(Debug, Clone, Copy)] +pub struct DocsService; + +impl Service for DocsService {} + +/// Progress during import operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ImportProgress { + /// Found the blob + Found { size: u64 }, + /// Progress + Progress { offset: u64 }, + /// Done + Done { hash: Hash }, + /// All done + AllDone, +} + +/// Mode for sharing documents +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ShareMode { + /// Share with read access + Read, + /// Share with write access + Write, +} + +// Request types +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OpenRequest { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OpenResponse; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CloseRequest { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CloseResponse; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StatusRequest { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StatusResponse { + pub status: OpenState, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ListRequest; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ListResponse { + pub id: NamespaceId, + pub capability: CapabilityKind, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CreateRequest; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CreateResponse { + pub id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DropRequest { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DropResponse; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ImportRequest { + pub capability: Capability, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ImportResponse { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SetRequest { + pub doc_id: NamespaceId, + pub author_id: AuthorId, + pub key: Bytes, + pub value: Bytes, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SetResponse { + pub entry: SignedEntry, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SetHashRequest { + pub doc_id: NamespaceId, + pub author_id: AuthorId, + pub key: Bytes, + pub hash: Hash, + pub size: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SetHashResponse; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GetManyRequest { + pub doc_id: NamespaceId, + pub query: Query, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GetExactRequest { + pub doc_id: NamespaceId, + pub key: Bytes, + pub author: AuthorId, + pub include_empty: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GetExactResponse { + pub entry: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ImportFileRequest { + pub doc_id: NamespaceId, + pub author_id: AuthorId, + pub key: Bytes, + pub path: PathBuf, + pub in_place: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ExportFileRequest { + pub entry: Entry, + pub path: PathBuf, + pub mode: ExportMode, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DelRequest { + pub doc_id: NamespaceId, + pub author_id: AuthorId, + pub prefix: Bytes, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DelResponse { + pub removed: usize, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StartSyncRequest { + pub doc_id: NamespaceId, + pub peers: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StartSyncResponse; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LeaveRequest { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LeaveResponse; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ShareRequest { + pub doc_id: NamespaceId, + pub mode: ShareMode, + pub addr_options: AddrInfoOptions, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ShareResponse(pub DocTicket); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SubscribeRequest { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SubscribeResponse { + pub event: LiveEvent, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GetDownloadPolicyRequest { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GetDownloadPolicyResponse { + pub policy: DownloadPolicy, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SetDownloadPolicyRequest { + pub doc_id: NamespaceId, + pub policy: DownloadPolicy, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SetDownloadPolicyResponse; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GetSyncPeersRequest { + pub doc_id: NamespaceId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GetSyncPeersResponse { + pub peers: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorListRequest; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorListResponse { + pub author_id: AuthorId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorCreateRequest; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorCreateResponse { + pub author_id: AuthorId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorGetDefaultRequest; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorGetDefaultResponse { + pub author_id: AuthorId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorSetDefaultRequest { + pub author_id: AuthorId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorSetDefaultResponse; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorImportRequest { + pub author: Author, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorImportResponse { + pub author_id: AuthorId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorExportRequest { + pub author: AuthorId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorExportResponse { + pub author: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorDeleteRequest { + pub author: AuthorId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthorDeleteResponse; + +// Use the macro to generate both the DocsProtocol and DocsMessage enums +// plus implement Channels for each type +#[rpc_requests(DocsService, message = DocsMessage)] +#[derive(Serialize, Deserialize, Debug)] +pub enum DocsProtocol { + #[rpc(tx = oneshot::Sender>)] + Open(OpenRequest), + #[rpc(tx = oneshot::Sender>)] + Close(CloseRequest), + #[rpc(tx = oneshot::Sender>)] + Status(StatusRequest), + #[rpc(tx = mpsc::Sender>)] + List(ListRequest), + #[rpc(tx = oneshot::Sender>)] + Create(CreateRequest), + #[rpc(tx = oneshot::Sender>)] + Drop(DropRequest), + #[rpc(tx = oneshot::Sender>)] + Import(ImportRequest), + #[rpc(tx = oneshot::Sender>)] + Set(SetRequest), + #[rpc(tx = oneshot::Sender>)] + SetHash(SetHashRequest), + #[rpc(tx = mpsc::Sender>)] + Get(GetManyRequest), + #[rpc(tx = oneshot::Sender>)] + GetExact(GetExactRequest), + // #[rpc(tx = mpsc::Sender)] + // ImportFile(ImportFileRequest), + // #[rpc(tx = mpsc::Sender)] + // ExportFile(ExportFileRequest), + #[rpc(tx = oneshot::Sender>)] + Del(DelRequest), + #[rpc(tx = oneshot::Sender>)] + StartSync(StartSyncRequest), + #[rpc(tx = oneshot::Sender>)] + Leave(LeaveRequest), + #[rpc(tx = oneshot::Sender>)] + Share(ShareRequest), + #[rpc(tx = mpsc::Sender>)] + Subscribe(SubscribeRequest), + #[rpc(tx = oneshot::Sender>)] + GetDownloadPolicy(GetDownloadPolicyRequest), + #[rpc(tx = oneshot::Sender>)] + SetDownloadPolicy(SetDownloadPolicyRequest), + #[rpc(tx = oneshot::Sender>)] + GetSyncPeers(GetSyncPeersRequest), + #[rpc(tx = mpsc::Sender>)] + AuthorList(AuthorListRequest), + #[rpc(tx = oneshot::Sender>)] + AuthorCreate(AuthorCreateRequest), + #[rpc(tx = oneshot::Sender>)] + AuthorGetDefault(AuthorGetDefaultRequest), + #[rpc(tx = oneshot::Sender>)] + AuthorSetDefault(AuthorSetDefaultRequest), + #[rpc(tx = oneshot::Sender>)] + AuthorImport(AuthorImportRequest), + #[rpc(tx = oneshot::Sender>)] + AuthorExport(AuthorExportRequest), + #[rpc(tx = oneshot::Sender>)] + AuthorDelete(AuthorDeleteRequest), +} + +/// Options to configure what is included in a [`iroh::NodeAddr`]. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + Default, + Debug, + derive_more::Display, + derive_more::FromStr, + Serialize, + Deserialize, +)] +pub enum AddrInfoOptions { + /// Only the Node ID is added. + /// + /// This usually means that iroh-dns discovery is used to find address information. + #[default] + Id, + /// Includes the Node ID and both the relay URL, and the direct addresses. + RelayAndAddresses, + /// Includes the Node ID and the relay URL. + Relay, + /// Includes the Node ID and the direct addresses. + Addresses, +} + +impl AddrInfoOptions { + /// Apply the options to the given address. + pub fn apply( + &self, + iroh::NodeAddr { + node_id, + relay_url, + direct_addresses, + }: &iroh::NodeAddr, + ) -> iroh::NodeAddr { + match self { + Self::Id => iroh::NodeAddr { + node_id: *node_id, + relay_url: None, + direct_addresses: Default::default(), + }, + Self::Relay => iroh::NodeAddr { + node_id: *node_id, + relay_url: relay_url.clone(), + direct_addresses: Default::default(), + }, + Self::Addresses => iroh::NodeAddr { + node_id: *node_id, + relay_url: None, + direct_addresses: direct_addresses.clone(), + }, + Self::RelayAndAddresses => iroh::NodeAddr { + node_id: *node_id, + relay_url: relay_url.clone(), + direct_addresses: direct_addresses.clone(), + }, + } + } +} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index f806f4b..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,1267 +0,0 @@ -//! Define commands for interacting with documents in Iroh. - -use std::{ - cell::RefCell, - collections::BTreeMap, - env, - path::{Path, PathBuf}, - rc::Rc, - str::FromStr, - sync::{Arc, RwLock}, - time::{Duration, Instant}, -}; - -use anyhow::{anyhow, bail, Context, Result}; -use clap::Parser; -use colored::Colorize; -use dialoguer::Confirm; -use futures_buffered::BufferedStreamExt; -use futures_lite::{Stream, StreamExt}; -use indicatif::{HumanBytes, HumanDuration, MultiProgress, ProgressBar, ProgressStyle}; -use iroh_blobs::{ - provider::AddProgress, - rpc::client::blobs::{self, WrapOption}, - util::{ - fs::{path_content_info, path_to_key, PathContent}, - SetTagOption, - }, - Hash, Tag, -}; -use serde::{Deserialize, Serialize}; -use tokio::io::AsyncReadExt; -use tracing::warn; - -use crate::{ - engine::Origin, - rpc::{ - client::docs::{self, Doc, Entry, LiveEvent, ShareMode}, - AddrInfoOptions, - }, - store::{DownloadPolicy, FilterKind, Query, SortDirection}, - AuthorId, ContentStatus, DocTicket, NamespaceId, -}; - -pub mod authors; - -type AuthorsClient = crate::rpc::client::authors::Client; - -const ENV_AUTHOR: &str = "IROH_AUTHOR"; -const ENV_DOC: &str = "IROH_DOC"; - -#[derive(Debug, Clone, Copy, Eq, PartialEq, strum::AsRefStr, strum::EnumString, strum::Display)] -pub(crate) enum ConsolePaths { - #[strum(serialize = "current-author")] - CurrentAuthor, - #[strum(serialize = "history")] - History, -} - -impl ConsolePaths { - fn root(iroh_data_dir: impl AsRef) -> PathBuf { - PathBuf::from(iroh_data_dir.as_ref()).join("console") - } - pub fn with_iroh_data_dir(self, iroh_data_dir: impl AsRef) -> PathBuf { - Self::root(iroh_data_dir).join(self.as_ref()) - } -} - -/// Environment for CLI and REPL -/// -/// This is cheaply cloneable and has interior mutability. If not running in the console -/// environment, `Self::set_doc` and `Self::set_author` will lead to an error, as changing the -/// environment is only supported within the console. -#[derive(Clone, Debug)] -pub struct ConsoleEnv(Arc>); - -#[derive(PartialEq, Eq, Debug, Deserialize, Serialize, Clone)] -struct ConsoleEnvInner { - /// Active author. Read from IROH_AUTHOR env variable. - /// For console also read from/persisted to a file. - /// Defaults to the node's default author if both are empty. - author: AuthorId, - /// Active doc. Read from IROH_DOC env variable. Not persisted. - doc: Option, - is_console: bool, - iroh_data_dir: PathBuf, -} - -impl ConsoleEnv { - /// Read from environment variables and the console config file. - pub async fn for_console( - iroh_data_dir: PathBuf, - authors: &crate::rpc::client::authors::Client, - ) -> Result { - let console_data_dir = ConsolePaths::root(&iroh_data_dir); - tokio::fs::create_dir_all(&console_data_dir) - .await - .with_context(|| { - format!( - "failed to create console data directory at `{}`", - console_data_dir.to_string_lossy() - ) - })?; - - Self::migrate_console_files_016_017(&iroh_data_dir).await?; - - let configured_author = Self::get_console_default_author(&iroh_data_dir)?; - let author = env_author(configured_author, authors).await?; - let env = ConsoleEnvInner { - author, - doc: env_doc()?, - is_console: true, - iroh_data_dir, - }; - Ok(Self(Arc::new(RwLock::new(env)))) - } - - /// Read only from environment variables. - pub async fn for_cli(iroh_data_dir: PathBuf, authors: &AuthorsClient) -> Result { - let author = env_author(None, authors).await?; - let env = ConsoleEnvInner { - author, - doc: env_doc()?, - is_console: false, - iroh_data_dir, - }; - Ok(Self(Arc::new(RwLock::new(env)))) - } - - fn get_console_default_author(iroh_data_root: &Path) -> anyhow::Result> { - let author_path = ConsolePaths::CurrentAuthor.with_iroh_data_dir(iroh_data_root); - if let Ok(s) = std::fs::read_to_string(&author_path) { - let author = AuthorId::from_str(&s).with_context(|| { - format!( - "Failed to parse author file at {}", - author_path.to_string_lossy() - ) - })?; - Ok(Some(author)) - } else { - Ok(None) - } - } - - /// True if running in a Iroh console session, false for a CLI command - pub(crate) fn is_console(&self) -> bool { - self.0.read().unwrap().is_console - } - - /// Return the iroh data directory - pub fn iroh_data_dir(&self) -> PathBuf { - self.0.read().unwrap().iroh_data_dir.clone() - } - - /// Set the active author. - /// - /// Will error if not running in the Iroh console. - /// Will persist to a file in the Iroh data dir otherwise. - pub(crate) fn set_author(&self, author: AuthorId) -> anyhow::Result<()> { - let author_path = ConsolePaths::CurrentAuthor.with_iroh_data_dir(self.iroh_data_dir()); - let mut inner = self.0.write().unwrap(); - if !inner.is_console { - bail!("Switching the author is only supported within the Iroh console, not on the command line"); - } - inner.author = author; - std::fs::write(author_path, author.to_string().as_bytes())?; - Ok(()) - } - - /// Set the active document. - /// - /// Will error if not running in the Iroh console. - /// Will not persist, only valid for the current console session. - pub(crate) fn set_doc(&self, doc: NamespaceId) -> anyhow::Result<()> { - let mut inner = self.0.write().unwrap(); - if !inner.is_console { - bail!("Switching the document is only supported within the Iroh console, not on the command line"); - } - inner.doc = Some(doc); - Ok(()) - } - - /// Get the active document. - pub fn doc(&self, arg: Option) -> anyhow::Result { - let inner = self.0.read().unwrap(); - let doc_id = arg.or(inner.doc).ok_or_else(|| { - anyhow!( - "Missing document id. Set the active document with the `IROH_DOC` environment variable or the `-d` option.\n\ - In the console, you can also set the active document with `doc switch`." - ) - })?; - Ok(doc_id) - } - - /// Get the active author. - /// - /// This is either the node's default author, or in the console optionally the author manually - /// switched to. - pub fn author(&self) -> AuthorId { - let inner = self.0.read().unwrap(); - inner.author - } - - pub(crate) async fn migrate_console_files_016_017(iroh_data_dir: &Path) -> Result<()> { - // In iroh up to 0.16, we stored console settings directly in the data directory. Starting - // from 0.17, they live in a subdirectory and have new paths. - let old_current_author = iroh_data_dir.join("default_author.pubkey"); - if old_current_author.is_file() { - if let Err(err) = tokio::fs::rename( - &old_current_author, - ConsolePaths::CurrentAuthor.with_iroh_data_dir(iroh_data_dir), - ) - .await - { - warn!(path=%old_current_author.to_string_lossy(), "failed to migrate the console's current author file: {err}"); - } - } - let old_history = iroh_data_dir.join("history"); - if old_history.is_file() { - if let Err(err) = tokio::fs::rename( - &old_history, - ConsolePaths::History.with_iroh_data_dir(iroh_data_dir), - ) - .await - { - warn!(path=%old_history.to_string_lossy(), "failed to migrate the console's history file: {err}"); - } - } - Ok(()) - } -} - -async fn env_author(from_config: Option, authors: &AuthorsClient) -> Result { - if let Some(author) = env::var(ENV_AUTHOR) - .ok() - .map(|s| { - s.parse() - .context("Failed to parse IROH_AUTHOR environment variable") - }) - .transpose()? - .or(from_config) - { - Ok(author) - } else { - authors.default().await - } -} - -fn env_doc() -> Result> { - env::var(ENV_DOC) - .ok() - .map(|s| { - s.parse() - .context("Failed to parse IROH_DOC environment variable") - }) - .transpose() -} - -/// The maximum length of content to display before truncating. -const MAX_DISPLAY_CONTENT_LEN: u64 = 80; - -/// Different modes to display content. -#[derive(Debug, Clone, Copy, clap::ValueEnum)] -pub enum DisplayContentMode { - /// Displays the content if small enough, otherwise it displays the content hash. - Auto, - /// Display the content unconditionally. - Content, - /// Display the hash of the content. - Hash, - /// Display the shortened hash of the content. - ShortHash, -} - -/// General download policy for a document. -#[derive(Debug, Clone, Copy, clap::ValueEnum, derive_more::Display)] -pub enum FetchKind { - /// Download everything in this document. - Everything, - /// Download nothing in this document. - Nothing, -} - -#[allow(missing_docs)] -/// Subcommands for the download policy command. -#[derive(Debug, Clone, clap::Subcommand)] -pub enum DlPolicyCmd { - Set { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - #[clap(short, long)] - doc: Option, - /// Set the general download policy for this document. - kind: FetchKind, - /// Add an exception to the download policy. - /// An exception must be formatted as `::`. - /// - /// - can be either `prefix` or `exact`. - /// - /// - `` can be either `utf8` or `hex`. - #[clap(short, long, value_name = "matching_kind>::, - }, - Get { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - #[clap(short, long)] - doc: Option, - }, -} - -/// Possible `Document` commands. -#[allow(missing_docs)] -#[derive(Debug, Clone, Parser)] -pub enum DocCommands { - /// Set the active document (only works within the Iroh console). - Switch { id: NamespaceId }, - /// Create a new document. - Create { - /// Switch to the created document (only in the Iroh console). - #[clap(long)] - switch: bool, - }, - /// Join a document from a ticket. - Join { - ticket: DocTicket, - /// Switch to the joined document (only in the Iroh console). - #[clap(long)] - switch: bool, - }, - /// List documents. - List, - /// Share a document with peers. - Share { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - #[clap(short, long)] - doc: Option, - /// The sharing mode. - mode: ShareMode, - /// Options to configure the address information in the generated ticket. - /// - /// Use `relay-and-addresses` in networks with no internet connectivity. - #[clap(long, default_value_t = AddrInfoOptions::Id)] - addr_options: AddrInfoOptions, - }, - /// Set an entry in a document. - Set { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - #[clap(short, long)] - doc: Option, - /// Author of the entry. - /// - /// Required unless the author is set through the IROH_AUTHOR environment variable. - /// Within the Iroh console, the active author can also set with `author switch`. - #[clap(long)] - author: Option, - /// Key to the entry (parsed as UTF-8 string). - key: String, - /// Content to store for this entry (parsed as UTF-8 string) - value: String, - }, - /// Set the download policies for a document. - #[clap(subcommand)] - DlPolicy(DlPolicyCmd), - /// Get entries in a document. - /// - /// Shows the author, content hash and content length for all entries for this key. - Get { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - #[clap(short, long)] - doc: Option, - /// Key to the entry (parsed as UTF-8 string). - key: String, - /// If true, get all entries that start with KEY. - #[clap(short, long)] - prefix: bool, - /// Filter by author. - #[clap(long)] - author: Option, - /// How to show the contents of the key. - #[clap(short, long, value_enum, default_value_t=DisplayContentMode::Auto)] - mode: DisplayContentMode, - }, - /// Delete all entries below a key prefix. - Del { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - #[clap(short, long)] - doc: Option, - /// Author of the entry. - /// - /// Required unless the author is set through the IROH_AUTHOR environment variable. - /// Within the Iroh console, the active author can also set with `author switch`. - #[clap(long)] - author: Option, - /// Prefix to delete. All entries whose key starts with or is equal to the prefix will be - /// deleted. - prefix: String, - }, - /// List all keys in a document. - #[clap(alias = "ls")] - Keys { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - #[clap(short, long)] - doc: Option, - /// Filter by author. - #[clap(long)] - author: Option, - /// Optional key prefix (parsed as UTF-8 string) - prefix: Option, - /// How to sort the entries - #[clap(long, default_value_t=Sorting::Author)] - sort: Sorting, - /// Sort in descending order - #[clap(long)] - desc: bool, - /// How to show the contents of the keys. - #[clap(short, long, value_enum, default_value_t=DisplayContentMode::ShortHash)] - mode: DisplayContentMode, - }, - /// Import data into a document - Import { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also be set with `doc switch`. - #[clap(short, long)] - doc: Option, - /// Author of the entry. - /// - /// Required unless the author is set through the IROH_AUTHOR environment variable. - /// Within the Iroh console, the active author can also be set with `author switch`. - #[clap(long)] - author: Option, - /// Prefix to add to imported entries (parsed as UTF-8 string). Defaults to no prefix - #[clap(long)] - prefix: Option, - /// Path to a local file or directory to import - /// - /// Pathnames will be used as the document key - path: String, - /// If true, don't copy the file into iroh, reference the existing file instead - /// - /// Moving a file imported with `in-place` will result in data corruption - #[clap(short, long)] - in_place: bool, - /// When true, you will not get a prompt to confirm you want to import the files - #[clap(long, default_value_t = false)] - no_prompt: bool, - }, - /// Export the most recent data for a key from a document - Export { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also be set with `doc switch`. - #[clap(short, long)] - doc: Option, - /// Key to the entry (parsed as UTF-8 string) - /// - /// When just the key is present, will export the latest entry for that key. - key: String, - /// Path to export to - #[clap(short, long)] - out: String, - }, - /// Watch for changes and events on a document - Watch { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - #[clap(short, long)] - doc: Option, - }, - /// Stop syncing a document. - Leave { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - doc: Option, - }, - /// Delete a document from the local node. - /// - /// This is a destructive operation. Both the document secret key and all entries in the - /// document will be permanently deleted from the node's storage. Content blobs will be deleted - /// through garbage collection unless they are referenced from another document or tag. - Drop { - /// Document to operate on. - /// - /// Required unless the document is set through the IROH_DOC environment variable. - /// Within the Iroh console, the active document can also set with `doc switch`. - doc: Option, - }, -} - -/// How to sort. -#[derive(clap::ValueEnum, Clone, Debug, Default, strum::Display)] -#[strum(serialize_all = "kebab-case")] -pub enum Sorting { - /// Sort by author, then key - #[default] - Author, - /// Sort by key, then author - Key, -} - -impl From for crate::store::SortBy { - fn from(value: Sorting) -> Self { - match value { - Sorting::Author => Self::AuthorKey, - Sorting::Key => Self::KeyAuthor, - } - } -} - -fn fmt_short(bytes: &[u8]) -> String { - let len = bytes.len().min(10); - data_encoding::BASE32_NOPAD - .encode(&bytes[..len]) - .to_ascii_lowercase() -} - -impl DocCommands { - /// Runs the document command given the iroh client and the console environment. - pub async fn run( - self, - docs: &docs::Client, - blobs: &blobs::Client, - env: &ConsoleEnv, - ) -> Result<()> { - match self { - Self::Switch { id: doc } => { - env.set_doc(doc)?; - println!("Active doc is now {}", fmt_short(doc.as_bytes())); - } - Self::Create { switch } => { - if switch && !env.is_console() { - bail!("The --switch flag is only supported within the Iroh console."); - } - - let doc = docs.create().await?; - println!("{}", doc.id()); - - if switch { - env.set_doc(doc.id())?; - println!("Active doc is now {}", fmt_short(doc.id().as_bytes())); - } - } - Self::Join { ticket, switch } => { - if switch && !env.is_console() { - bail!("The --switch flag is only supported within the Iroh console."); - } - - let doc = docs.import(ticket).await?; - println!("{}", doc.id()); - - if switch { - env.set_doc(doc.id())?; - println!("Active doc is now {}", fmt_short(doc.id().as_bytes())); - } - } - Self::List => { - let mut stream = docs.list().await?; - while let Some((id, kind)) = stream.try_next().await? { - println!("{id} {kind}") - } - } - Self::Share { - doc, - mode, - addr_options, - } => { - let doc = get_doc(docs, env, doc).await?; - let ticket = doc.share(mode, addr_options).await?; - println!("{}", ticket); - } - Self::Set { - doc, - author, - key, - value, - } => { - let doc = get_doc(docs, env, doc).await?; - let author = author.unwrap_or(env.author()); - let key = key.as_bytes().to_vec(); - let value = value.as_bytes().to_vec(); - let hash = doc.set_bytes(author, key, value).await?; - println!("{}", hash); - } - Self::Del { - doc, - author, - prefix, - } => { - let doc = get_doc(docs, env, doc).await?; - let author = author.unwrap_or(env.author()); - let prompt = - format!("Deleting all entries whose key starts with {prefix}. Continue?"); - if Confirm::new() - .with_prompt(prompt) - .interact() - .unwrap_or(false) - { - let key = prefix.as_bytes().to_vec(); - let removed = doc.del(author, key).await?; - println!("Deleted {removed} entries."); - println!( - "Inserted an empty entry for author {} with key {prefix}.", - fmt_short(author.as_bytes()) - ); - } else { - println!("Aborted.") - } - } - Self::Get { - doc, - key, - prefix, - author, - mode, - } => { - let doc = get_doc(docs, env, doc).await?; - let key = key.as_bytes().to_vec(); - let query = Query::all(); - let query = match (author, prefix) { - (None, false) => query.key_exact(key), - (None, true) => query.key_prefix(key), - (Some(author), true) => query.author(author).key_prefix(key), - (Some(author), false) => query.author(author).key_exact(key), - }; - - let mut stream = doc.get_many(query).await?; - while let Some(entry) = stream.try_next().await? { - println!("{}", fmt_entry(blobs, &entry, mode).await); - } - } - Self::Keys { - doc, - prefix, - author, - mode, - sort, - desc, - } => { - let doc = get_doc(docs, env, doc).await?; - let mut query = Query::all(); - if let Some(author) = author { - query = query.author(author); - } - if let Some(prefix) = prefix { - query = query.key_prefix(prefix); - } - let direction = match desc { - true => SortDirection::Desc, - false => SortDirection::Asc, - }; - query = query.sort_by(sort.into(), direction); - let mut stream = doc.get_many(query).await?; - while let Some(entry) = stream.try_next().await? { - println!("{}", fmt_entry(blobs, &entry, mode).await); - } - } - Self::Leave { doc } => { - let doc = get_doc(docs, env, doc).await?; - doc.leave().await?; - println!("Doc {} is now inactive", fmt_short(doc.id().as_bytes())); - } - Self::Import { - doc, - author, - prefix, - path, - in_place, - no_prompt, - } => { - let doc = get_doc(docs, env, doc).await?; - let author = author.unwrap_or(env.author()); - let mut prefix = prefix.unwrap_or_else(|| String::from("")); - - if prefix.ends_with('/') { - prefix.pop(); - } - let root = canonicalize_path(&path)?.canonicalize()?; - let tag = tag_from_file_name(&root)?; - - let root0 = root.clone(); - println!("Preparing import..."); - // get information about the directory or file we are trying to import - // and confirm with the user that they still want to import the file - let PathContent { size, files } = - tokio::task::spawn_blocking(|| path_content_info(root0)).await??; - if !no_prompt { - let prompt = format!("Import {files} files totaling {}?", HumanBytes(size)); - if !Confirm::new() - .with_prompt(prompt) - .interact() - .unwrap_or(false) - { - println!("Aborted."); - return Ok(()); - } else { - print!("\r"); - } - } - - let stream = blobs - .add_from_path( - root.clone(), - in_place, - SetTagOption::Named(tag.clone()), - WrapOption::NoWrap, - ) - .await?; - let root_prefix = match root.parent() { - Some(p) => p.to_path_buf(), - None => PathBuf::new(), - }; - let start = Instant::now(); - import_coordinator(doc, author, root_prefix, prefix, stream, size, files).await?; - println!("Success! ({})", HumanDuration(start.elapsed())); - } - Self::Export { doc, key, out } => { - let doc = get_doc(docs, env, doc).await?; - let key_str = key.clone(); - let key = key.as_bytes().to_vec(); - let path: PathBuf = canonicalize_path(&out)?; - let mut stream = doc.get_many(Query::key_exact(key)).await?; - let entry = match stream.try_next().await? { - None => { - println!(""); - return Ok(()); - } - Some(e) => e, - }; - match blobs.read(entry.content_hash()).await { - Ok(mut content) => { - if let Some(dir) = path.parent() { - if let Err(err) = std::fs::create_dir_all(dir) { - println!( - "", - path.display() - ); - } - }; - let pb = ProgressBar::new(content.size()); - pb.set_style(ProgressStyle::default_bar() - .template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, eta {eta})").unwrap() - .progress_chars("=>-")); - let file = tokio::fs::File::create(path.clone()).await?; - if let Err(err) = - tokio::io::copy(&mut content, &mut pb.wrap_async_write(file)).await - { - pb.finish_and_clear(); - println!("", path.display()) - } else { - pb.finish_and_clear(); - println!("wrote '{key_str}' to {}", path.display()); - } - } - Err(err) => println!(""), - } - } - Self::Watch { doc } => { - let doc = get_doc(docs, env, doc).await?; - let mut stream = doc.subscribe().await?; - while let Some(event) = stream.next().await { - let event = event?; - match event { - LiveEvent::InsertLocal { entry } => { - println!( - "local change: {}", - fmt_entry(blobs, &entry, DisplayContentMode::Auto).await - ) - } - LiveEvent::InsertRemote { - entry, - from, - content_status, - } => { - let content = match content_status { - ContentStatus::Complete => { - fmt_entry(blobs, &entry, DisplayContentMode::Auto).await - } - ContentStatus::Incomplete => { - let (Ok(content) | Err(content)) = - fmt_content(blobs, &entry, DisplayContentMode::ShortHash) - .await; - format!("", content, human_len(&entry)) - } - ContentStatus::Missing => { - let (Ok(content) | Err(content)) = - fmt_content(blobs, &entry, DisplayContentMode::ShortHash) - .await; - format!("", content, human_len(&entry)) - } - }; - println!( - "remote change via @{}: {}", - fmt_short(from.as_bytes()), - content - ) - } - LiveEvent::ContentReady { hash } => { - println!("content ready: {}", fmt_short(hash.as_bytes())) - } - LiveEvent::SyncFinished(event) => { - let origin = match event.origin { - Origin::Accept => "they initiated", - Origin::Connect(_) => "we initiated", - }; - match event.result { - Ok(details) => { - println!( - "synced peer {} ({origin}, received {}, sent {}", - fmt_short(event.peer.as_bytes()), - details.entries_received, - details.entries_sent - ) - } - Err(err) => println!( - "failed to sync with peer {} ({origin}): {err}", - fmt_short(event.peer.as_bytes()) - ), - } - } - LiveEvent::NeighborUp(peer) => { - println!("neighbor peer up: {peer:?}"); - } - LiveEvent::NeighborDown(peer) => { - println!("neighbor peer down: {peer:?}"); - } - LiveEvent::PendingContentReady => { - println!("all pending content is now ready") - } - } - } - } - Self::Drop { doc } => { - let doc = get_doc(docs, env, doc).await?; - println!( - "Deleting a document will permanently remove the document secret key, all document entries, \n\ - and all content blobs which are not referenced from other docs or tags." - ); - let prompt = format!("Delete document {}?", fmt_short(doc.id().as_bytes())); - if Confirm::new() - .with_prompt(prompt) - .interact() - .unwrap_or(false) - { - docs.drop_doc(doc.id()).await?; - println!("Doc {} has been deleted.", fmt_short(doc.id().as_bytes())); - } else { - println!("Aborted.") - } - } - Self::DlPolicy(DlPolicyCmd::Set { doc, kind, except }) => { - let doc = get_doc(docs, env, doc).await?; - let download_policy = match kind { - FetchKind::Everything => DownloadPolicy::EverythingExcept(except), - FetchKind::Nothing => DownloadPolicy::NothingExcept(except), - }; - if let Err(e) = doc.set_download_policy(download_policy).await { - println!("Could not set the document's download policy. {e}") - } - } - Self::DlPolicy(DlPolicyCmd::Get { doc }) => { - let doc = get_doc(docs, env, doc).await?; - match doc.get_download_policy().await { - Ok(dl_policy) => { - let (kind, exceptions) = match dl_policy { - DownloadPolicy::NothingExcept(exceptions) => { - (FetchKind::Nothing, exceptions) - } - DownloadPolicy::EverythingExcept(exceptions) => { - (FetchKind::Everything, exceptions) - } - }; - println!("Download {kind} in this document."); - if !exceptions.is_empty() { - println!("Exceptions:"); - for exception in exceptions { - println!("{exception}") - } - } - } - Err(x) => { - println!("Could not get the document's download policy: {x}") - } - } - } - } - Ok(()) - } -} - -/// Gets the document given the client, the environment (and maybe the [`crate::keys::NamespaceId`]). -async fn get_doc( - docs: &docs::Client, - env: &ConsoleEnv, - id: Option, -) -> anyhow::Result { - let doc_id = env.doc(id)?; - docs.open(doc_id).await?.context("Document not found") -} - -/// Formats the content. If an error occurs it's returned in a formatted, friendly way. -async fn fmt_content( - blobs: &blobs::Client, - entry: &Entry, - mode: DisplayContentMode, -) -> Result { - let read_failed = |err: anyhow::Error| format!(""); - let encode_hex = |err: std::string::FromUtf8Error| format!("0x{}", hex::encode(err.as_bytes())); - let as_utf8 = |buf: Vec| String::from_utf8(buf).map(|repr| format!("\"{repr}\"")); - - match mode { - DisplayContentMode::Auto => { - if entry.content_len() < MAX_DISPLAY_CONTENT_LEN { - // small content: read fully as UTF-8 - let bytes = blobs - .read_to_bytes(entry.content_hash()) - .await - .map_err(read_failed)?; - Ok(as_utf8(bytes.into()).unwrap_or_else(encode_hex)) - } else { - // large content: read just the first part as UTF-8 - let mut blob_reader = blobs - .read(entry.content_hash()) - .await - .map_err(read_failed)?; - let mut buf = Vec::with_capacity(MAX_DISPLAY_CONTENT_LEN as usize + 5); - - blob_reader - .read_buf(&mut buf) - .await - .map_err(|io_err| read_failed(io_err.into()))?; - let mut repr = as_utf8(buf).unwrap_or_else(encode_hex); - // let users know this is not shown in full - repr.push_str("..."); - Ok(repr) - } - } - DisplayContentMode::Content => { - // read fully as UTF-8 - let bytes = blobs - .read_to_bytes(entry.content_hash()) - .await - .map_err(read_failed)?; - Ok(as_utf8(bytes.into()).unwrap_or_else(encode_hex)) - } - DisplayContentMode::ShortHash => { - let hash = entry.content_hash(); - Ok(fmt_short(hash.as_bytes())) - } - DisplayContentMode::Hash => { - let hash = entry.content_hash(); - Ok(hash.to_string()) - } - } -} - -/// Converts the [`Entry`] to human-readable bytes. -fn human_len(entry: &Entry) -> HumanBytes { - HumanBytes(entry.content_len()) -} - -/// Formats an entry for display as a `String`. -#[must_use = "this won't be printed, you need to print it yourself"] -async fn fmt_entry(blobs: &blobs::Client, entry: &Entry, mode: DisplayContentMode) -> String { - let key = std::str::from_utf8(entry.key()) - .unwrap_or("") - .bold(); - let author = fmt_short(entry.author().as_bytes()); - let (Ok(content) | Err(content)) = fmt_content(blobs, entry, mode).await; - let len = human_len(entry); - format!("@{author}: {key} = {content} ({len})") -} - -/// Converts a path to a canonical path. -fn canonicalize_path(path: &str) -> anyhow::Result { - let path = PathBuf::from(shellexpand::tilde(&path).to_string()); - Ok(path) -} - -/// Creates a [`Tag`] from a file name (given as a [`Path`]). -fn tag_from_file_name(path: &Path) -> anyhow::Result { - match path.file_name() { - Some(name) => name - .to_os_string() - .into_string() - .map(|t| t.into()) - .map_err(|e| anyhow!("{e:?} contains invalid Unicode")), - None => bail!("the given `path` does not have a proper directory or file name"), - } -} - -/// Takes the `BlobsClient::add_from_path` and coordinates adding blobs to a -/// document via the hash of the blob. It also creates and powers the -/// `ImportProgressBar`. -#[tracing::instrument(skip_all)] -async fn import_coordinator( - doc: Doc, - author_id: AuthorId, - root: PathBuf, - prefix: String, - blob_add_progress: impl Stream> + Send + Unpin + 'static, - expected_size: u64, - expected_entries: u64, -) -> Result<()> { - let imp = ImportProgressBar::new( - &root.display().to_string(), - doc.id(), - expected_size, - expected_entries, - ); - let task_imp = imp.clone(); - - let collections = Rc::new(RefCell::new(BTreeMap::< - u64, - (String, u64, Option, u64), - >::new())); - - let doc2 = doc.clone(); - let imp2 = task_imp.clone(); - - let _stats: Vec<_> = blob_add_progress - .filter_map(|item| { - let item = match item.context("Error adding files") { - Err(e) => return Some(Err(e)), - Ok(item) => item, - }; - match item { - AddProgress::Found { name, id, size } => { - tracing::info!("Found({id},{name},{size})"); - imp.add_found(name.clone(), size); - collections.borrow_mut().insert(id, (name, size, None, 0)); - None - } - AddProgress::Progress { id, offset } => { - tracing::info!("Progress({id}, {offset})"); - if let Some((_, size, _, last_val)) = collections.borrow_mut().get_mut(&id) { - assert!(*last_val <= offset, "wtf"); - assert!(offset <= *size, "wtf2"); - imp.add_progress(offset - *last_val); - *last_val = offset; - } - None - } - AddProgress::Done { hash, id } => { - tracing::info!("Done({id},{hash:?})"); - match collections.borrow_mut().get_mut(&id) { - Some((path_str, size, ref mut h, last_val)) => { - imp.add_progress(*size - *last_val); - imp.import_found(path_str.clone()); - let path = PathBuf::from(path_str.clone()); - *h = Some(hash); - let key = - match path_to_key(path, Some(prefix.clone()), Some(root.clone())) { - Ok(k) => k.to_vec(), - Err(e) => { - tracing::info!( - "error getting key from {}, id {id}", - path_str - ); - return Some(Err(anyhow::anyhow!( - "Issue creating a key for entry {hash:?}: {e}" - ))); - } - }; - // send update to doc - tracing::info!( - "setting entry {} (id: {id}) to doc", - String::from_utf8(key.clone()).unwrap() - ); - Some(Ok((key, hash, *size))) - } - None => { - tracing::info!( - "error: got `AddProgress::Done` for unknown collection id {id}" - ); - Some(Err(anyhow::anyhow!( - "Received progress information on an unknown file." - ))) - } - } - } - AddProgress::AllDone { hash, .. } => { - imp.add_done(); - tracing::info!("AddProgress::AllDone({hash:?})"); - None - } - AddProgress::Abort(e) => { - tracing::info!("Error while adding data: {e}"); - Some(Err(anyhow::anyhow!("Error while adding files: {e}"))) - } - } - }) - .map(move |res| { - let doc = doc2.clone(); - let imp = imp2.clone(); - async move { - match res { - Ok((key, hash, size)) => { - let doc = doc.clone(); - doc.set_hash(author_id, key, hash, size).await?; - imp.import_progress(); - Ok(size) - } - Err(err) => Err(err), - } - } - }) - .buffered_unordered(128) - .try_collect() - .await?; - - task_imp.all_done(); - Ok(()) -} - -/// Progress bar for importing files. -#[derive(Debug, Clone)] -struct ImportProgressBar { - mp: MultiProgress, - import: ProgressBar, - add: ProgressBar, -} - -impl ImportProgressBar { - /// Creates a new import progress bar. - fn new(source: &str, doc_id: NamespaceId, expected_size: u64, expected_entries: u64) -> Self { - let mp = MultiProgress::new(); - let add = mp.add(ProgressBar::new(0)); - add.set_style(ProgressStyle::default_bar() - .template("{msg}\n{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, eta {eta})").unwrap() - .progress_chars("=>-")); - add.set_message(format!("Importing from {source}...")); - add.set_length(expected_size); - add.set_position(0); - add.enable_steady_tick(Duration::from_millis(500)); - - let doc_id = fmt_short(doc_id.as_bytes()); - let import = mp.add(ProgressBar::new(0)); - import.set_style(ProgressStyle::default_bar() - .template("{msg}\n{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} ({per_sec}, eta {eta})").unwrap() - .progress_chars("=>-")); - import.set_message(format!("Adding to doc {doc_id}...")); - import.set_length(expected_entries); - import.set_position(0); - import.enable_steady_tick(Duration::from_millis(500)); - - Self { mp, import, add } - } - - fn add_found(&self, _name: String, _size: u64) {} - - fn import_found(&self, _name: String) {} - - /// Marks having made some progress to the progress bar. - fn add_progress(&self, size: u64) { - self.add.inc(size); - } - - /// Marks having made one unit of progress on the import progress bar. - fn import_progress(&self) { - self.import.inc(1); - } - - /// Sets the `add` progress bar as completed. - fn add_done(&self) { - self.add.set_position(self.add.length().unwrap_or_default()); - } - - /// Sets the all progress bars as done. - fn all_done(self) { - self.mp.clear().ok(); - } -} - -#[cfg(test)] -mod tests { - // use super::*; - - // #[tokio::test] - // #[ignore] - // #[allow(unused_variables, unreachable_code, clippy::diverging_sub_expression)] - // async fn test_doc_import() -> Result<()> { - // let temp_dir = tempfile::tempdir().context("tempdir")?; - - // tokio::fs::create_dir_all(temp_dir.path()) - // .await - // .context("create dir all")?; - - // let foobar = temp_dir.path().join("foobar"); - // tokio::fs::write(foobar, "foobar") - // .await - // .context("write foobar")?; - // let foo = temp_dir.path().join("foo"); - // tokio::fs::write(foo, "foo").await.context("write foo")?; - - // let data_dir = tempfile::tempdir()?; - - // // let node = crate::commands::start::start_node(data_dir.path(), None, None).await?; - // // let node = todo!(); - // // let client = node.client(); - // let docs: docs::Client = todo!(); - // let authors = docs.authors(); - // let doc = docs.create().await.context("doc create")?; - // let author = authors.create().await.context("author create")?; - - // // set up command, getting iroh node - // let cli = ConsoleEnv::for_console(data_dir.path().to_owned(), &authors) - // .await - // .context("ConsoleEnv")?; - // // let iroh = iroh::client::Iroh::connect_path(data_dir.path()) - // // .await - // // .context("rpc connect")?; - // // let iroh = todo!(); - // let docs = todo!(); - // let blobs = todo!(); - - // let command = DocCommands::Import { - // doc: Some(doc.id()), - // author: Some(author), - // prefix: None, - // path: temp_dir.path().to_string_lossy().into(), - // in_place: false, - // no_prompt: true, - // }; - - // command - // .run(&docs, &blobs, &cli) - // .await - // .context("DocCommands run")?; - - // let keys: Vec<_> = doc - // .get_many(Query::all()) - // .await - // .context("doc get many")? - // .try_collect() - // .await?; - // assert_eq!(2, keys.len()); - - // // todo - // // iroh.shutdown(false).await?; - // Ok(()) - // } -} diff --git a/src/cli/authors.rs b/src/cli/authors.rs deleted file mode 100644 index 6cfe04e..0000000 --- a/src/cli/authors.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Define the commands to manage authors. - -use anyhow::{bail, Result}; -use clap::Parser; -use derive_more::FromStr; -use futures_lite::StreamExt; - -use super::{AuthorsClient, ConsoleEnv}; -use crate::{cli::fmt_short, Author, AuthorId}; - -#[allow(missing_docs)] -/// Commands to manage authors. -#[derive(Debug, Clone, Parser)] -pub enum AuthorCommands { - /// Set the active author (Note: only works within the Iroh console). - Switch { author: AuthorId }, - /// Create a new author. - Create { - /// Switch to the created author (Note: only works in the Iroh console). - #[clap(long)] - switch: bool, - }, - /// Delete an author. - Delete { author: AuthorId }, - /// Export an author. - Export { author: AuthorId }, - /// Import an author. - Import { author: String }, - /// Print the default author for this node. - Default { - /// Switch to the default author (Note: only works in the Iroh console). - #[clap(long)] - switch: bool, - }, - /// List authors. - #[clap(alias = "ls")] - List, -} - -impl AuthorCommands { - /// Runs the author command given an iroh client and console environment. - pub async fn run(self, authors: &AuthorsClient, env: &ConsoleEnv) -> Result<()> { - match self { - Self::Switch { author } => { - env.set_author(author)?; - println!("Active author is now {}", fmt_short(author.as_bytes())); - } - Self::List => { - let mut stream = authors.list().await?; - while let Some(author_id) = stream.try_next().await? { - println!("{}", author_id); - } - } - Self::Default { switch } => { - if switch && !env.is_console() { - bail!("The --switch flag is only supported within the Iroh console."); - } - let author_id = authors.default().await?; - println!("{}", author_id); - if switch { - env.set_author(author_id)?; - println!("Active author is now {}", fmt_short(author_id.as_bytes())); - } - } - Self::Create { switch } => { - if switch && !env.is_console() { - bail!("The --switch flag is only supported within the Iroh console."); - } - - let author_id = authors.create().await?; - println!("{}", author_id); - - if switch { - env.set_author(author_id)?; - println!("Active author is now {}", fmt_short(author_id.as_bytes())); - } - } - Self::Delete { author } => { - authors.delete(author).await?; - println!("Deleted author {}", fmt_short(author.as_bytes())); - } - Self::Export { author } => match authors.export(author).await? { - Some(author) => { - println!("{}", author); - } - None => { - println!("No author found {}", fmt_short(author.as_bytes())); - } - }, - Self::Import { author } => match Author::from_str(&author) { - Ok(author) => { - let id = author.id(); - authors.import(author).await?; - println!("Imported {}", fmt_short(id.as_bytes())); - } - Err(err) => { - eprintln!("Invalid author key: {}", err); - } - }, - } - Ok(()) - } -} diff --git a/src/engine.rs b/src/engine.rs index c8b59c4..fc6e903 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,7 +3,6 @@ //! [`crate::Replica`] is also called documents here. use std::{ - io, path::PathBuf, str::FromStr, sync::{Arc, RwLock}, @@ -13,14 +12,15 @@ use anyhow::{bail, Context, Result}; use futures_lite::{Stream, StreamExt}; use iroh::{Endpoint, NodeAddr, PublicKey}; use iroh_blobs::{ - downloader::Downloader, net_protocol::ProtectCb, store::EntryStatus, - util::local_pool::LocalPoolHandle, Hash, + api::{blobs::BlobStatus, downloader::Downloader, Store}, + store::fs::options::{ProtectCb, ProtectOutcome}, + Hash, }; use iroh_gossip::net::Gossip; use serde::{Deserialize, Serialize}; use tokio::sync::{mpsc, oneshot}; use tokio_util::task::AbortOnDropHandle; -use tracing::{error, error_span, Instrument}; +use tracing::{debug, error, error_span, Instrument}; use self::live::{LiveActor, ToLiveActor}; pub use self::{ @@ -44,7 +44,7 @@ const SUBSCRIBE_CHANNEL_CAP: usize = 256; /// The sync engine coordinates actors that manage open documents, set-reconciliation syncs with /// peers and a gossip swarm for each syncing document. #[derive(derive_more::Debug)] -pub struct Engine { +pub struct Engine { /// [`Endpoint`] used by the engine. pub endpoint: Endpoint, /// Handle to the actor thread. @@ -56,33 +56,68 @@ pub struct Engine { actor_handle: AbortOnDropHandle<()>, #[debug("ContentStatusCallback")] content_status_cb: ContentStatusCallback, - local_pool_handle: LocalPoolHandle, - blob_store: D, + blob_store: iroh_blobs::api::Store, + _gc_protect_task: AbortOnDropHandle<()>, } -impl Engine { +impl Engine { /// Start the sync engine. /// /// This will spawn two tokio tasks for the live sync coordination and gossip actors, and a - /// thread for the [`crate::actor::SyncHandle`]. + /// thread for the actor interacting with doc storage. pub async fn spawn( endpoint: Endpoint, gossip: Gossip, replica_store: crate::store::Store, - bao_store: D, + bao_store: iroh_blobs::api::Store, downloader: Downloader, default_author_storage: DefaultAuthorStorage, - local_pool_handle: LocalPoolHandle, + protect_cb: Option, ) -> anyhow::Result { let (live_actor_tx, to_live_actor_recv) = mpsc::channel(ACTOR_CHANNEL_CAP); let me = endpoint.node_id().fmt_short(); - let content_status_cb = { - let bao_store = bao_store.clone(); - Arc::new(move |hash| entry_to_content_status(bao_store.entry_status_sync(&hash))) + let content_status_cb: ContentStatusCallback = { + let blobs = bao_store.blobs().clone(); + Arc::new(move |hash: iroh_blobs::Hash| { + let blobs = blobs.clone(); + Box::pin(async move { + let blob_status = blobs.status(hash).await; + entry_to_content_status(blob_status) + }) + }) }; let sync = SyncHandle::spawn(replica_store, Some(content_status_cb.clone()), me.clone()); + let sync2 = sync.clone(); + let gc_protect_task = AbortOnDropHandle::new(n0_future::task::spawn(async move { + let Some(mut protect_handler) = protect_cb else { + return; + }; + while let Some(reply_tx) = protect_handler.0.recv().await { + let (tx, rx) = mpsc::channel(64); + if let Err(_err) = reply_tx.send(rx) { + continue; + } + let hashes = match sync2.content_hashes().await { + Ok(hashes) => hashes, + Err(err) => { + debug!("protect task: getting content hashes failed with {err:#}"); + if let Err(_err) = tx.send(Err(err)).await { + debug!("protect task: failed to forward error"); + } + continue; + } + }; + for hash in hashes { + if let Err(_err) = tx.send(hash).await { + debug!("protect task: failed to forward hash"); + break; + } + } + } + })); + let actor = LiveActor::new( sync.clone(), endpoint.clone(), @@ -119,41 +154,42 @@ impl Engine { actor_handle: AbortOnDropHandle::new(actor_handle), content_status_cb, default_author, - local_pool_handle, blob_store: bao_store, + _gc_protect_task: gc_protect_task, }) } - /// Return a callback that can be added to blobs to protect the content of - /// all docs from garbage collection. - pub fn protect_cb(&self) -> ProtectCb { - let sync = self.sync.clone(); - Box::new(move |live| { - let sync = sync.clone(); - Box::pin(async move { - let doc_hashes = match sync.content_hashes().await { - Ok(hashes) => hashes, - Err(err) => { - tracing::warn!("Error getting doc hashes: {}", err); - return; - } - }; - for hash in doc_hashes { - match hash { - Ok(hash) => { - live.insert(hash); - } - Err(err) => { - tracing::error!("Error getting doc hash: {}", err); - } - } - } - }) - }) - } + // TODO(Frando): We can't port iroh-docs to 0.90 without something like this. + // /// Return a callback that can be added to blobs to protect the content of + // /// all docs from garbage collection. + // pub fn protect_cb(&self) -> ProtectCb { + // let sync = self.sync.clone(); + // Box::new(move |live| { + // let sync = sync.clone(); + // Box::pin(async move { + // let doc_hashes = match sync.content_hashes().await { + // Ok(hashes) => hashes, + // Err(err) => { + // tracing::warn!("Error getting doc hashes: {}", err); + // return; + // } + // }; + // for hash in doc_hashes { + // match hash { + // Ok(hash) => { + // live.insert(hash); + // } + // Err(err) => { + // tracing::error!("Error getting doc hash: {}", err); + // } + // } + // } + // }) + // }) + // } /// Get the blob store. - pub fn blob_store(&self) -> &D { + pub fn blob_store(&self) -> &Store { &self.blob_store } @@ -201,17 +237,18 @@ impl Engine { &self, namespace: NamespaceId, ) -> Result> + Unpin + 'static> { - let content_status_cb = self.content_status_cb.clone(); - // Create a future that sends channel senders to the respective actors. // We clone `self` so that the future does not capture any lifetimes. - let this = self; + let content_status_cb = self.content_status_cb.clone(); // Subscribe to insert events from the replica. let a = { let (s, r) = async_channel::bounded(SUBSCRIBE_CHANNEL_CAP); - this.sync.subscribe(namespace, s).await?; - Box::pin(r).map(move |ev| LiveEvent::from_replica_event(ev, &content_status_cb)) + self.sync.subscribe(namespace, s).await?; + Box::pin(r).then(move |ev| { + let content_status_cb = content_status_cb.clone(); + Box::pin(async move { LiveEvent::from_replica_event(ev, &content_status_cb).await }) + }) }; // Subscribe to events from the [`live::Actor`]. @@ -219,7 +256,7 @@ impl Engine { let (s, r) = async_channel::bounded(SUBSCRIBE_CHANNEL_CAP); let r = Box::pin(r); let (reply, reply_rx) = oneshot::channel(); - this.to_live_actor + self.to_live_actor .send(ToLiveActor::Subscribe { namespace, sender: s, @@ -250,19 +287,14 @@ impl Engine { reply_rx.await?; Ok(()) } - - /// Returns the stored `LocalPoolHandle`. - pub fn local_pool_handle(&self) -> &LocalPoolHandle { - &self.local_pool_handle - } } -/// Converts an [`EntryStatus`] into a ['ContentStatus']. -pub fn entry_to_content_status(entry: io::Result) -> ContentStatus { +/// Converts an [`BlobStatus`] into a ['ContentStatus']. +fn entry_to_content_status(entry: irpc::Result) -> ContentStatus { match entry { - Ok(EntryStatus::Complete) => ContentStatus::Complete, - Ok(EntryStatus::Partial) => ContentStatus::Incomplete, - Ok(EntryStatus::NotFound) => ContentStatus::Missing, + Ok(BlobStatus::Complete { .. }) => ContentStatus::Complete, + Ok(BlobStatus::Partial { .. }) => ContentStatus::Incomplete, + Ok(BlobStatus::NotFound) => ContentStatus::Missing, Err(cause) => { tracing::warn!("Error while checking entry status: {cause:?}"); ContentStatus::Missing @@ -323,7 +355,7 @@ impl From for LiveEvent { } impl LiveEvent { - fn from_replica_event( + async fn from_replica_event( ev: crate::Event, content_status_cb: &ContentStatusCallback, ) -> Result { @@ -332,7 +364,7 @@ impl LiveEvent { entry: entry.into(), }, crate::Event::RemoteInsert { entry, from, .. } => Self::InsertRemote { - content_status: content_status_cb(entry.content_hash()), + content_status: content_status_cb(entry.content_hash()).await, entry: entry.into(), from: PublicKey::from_bytes(&from)?, }, @@ -458,3 +490,68 @@ impl DefaultAuthor { Ok(()) } } + +#[derive(Debug)] +struct ProtectCallbackSender(mpsc::Sender>>>); + +/// The handler for a blobs protection callback. +/// +/// See [`ProtectCallbackHandler::new`]. +#[derive(Debug)] +pub struct ProtectCallbackHandler( + pub(crate) mpsc::Receiver>>>, +); + +impl ProtectCallbackHandler { + /// Creates a callback and handler to manage blob protection. + /// + /// The returned [`ProtectCb`] must be passed set in the [`GcConfig`] of the [`iroh_blobs`] store where + /// the blobs for hashes in documents are persisted. The [`ProtectCallbackHandler`] must be passed to + /// [`Builder::protect_handler`] (or [`Engine::spawn`]). This will then ensure that hashes referenced + /// in docs will not be deleted from the blobs store, and will be garbage collected if they no longer appear + /// in any doc. + /// + /// [`Builder::protect_handler`]: crate::protocol::Builder::protect_handler + /// [`GcConfig`]: iroh_blobs::store::fs::options::GcConfig + pub fn new() -> (Self, ProtectCb) { + let (tx, rx) = mpsc::channel(4); + let cb = ProtectCallbackSender(tx).into_cb(); + let handler = ProtectCallbackHandler(rx); + (handler, cb) + } +} + +impl ProtectCallbackSender { + fn into_cb(self) -> ProtectCb { + let start_tx = self.0.clone(); + Arc::new(move |live| { + let start_tx = start_tx.clone(); + Box::pin(async move { + let (tx, rx) = oneshot::channel(); + if let Err(_err) = start_tx.send(tx).await { + tracing::warn!("Failed to get protected hashes from docs: ProtectCallback receiver dropped"); + return ProtectOutcome::Abort; + } + let mut rx = match rx.await { + Ok(rx) => rx, + Err(_err) => { + tracing::warn!("Failed to get protected hashes from docs: ProtectCallback sender dropped"); + return ProtectOutcome::Abort; + } + }; + while let Some(res) = rx.recv().await { + match res { + Err(err) => { + tracing::warn!("Getting protected hashes produces error: {err:#}"); + return ProtectOutcome::Abort; + } + Ok(hash) => { + live.insert(hash); + } + } + } + ProtectOutcome::Continue + }) + }) + } +} diff --git a/src/engine/gossip.rs b/src/engine/gossip.rs index 72f70db..102db79 100644 --- a/src/engine/gossip.rs +++ b/src/engine/gossip.rs @@ -5,7 +5,10 @@ use bytes::Bytes; use futures_lite::StreamExt; use futures_util::FutureExt; use iroh::NodeId; -use iroh_gossip::net::{Event, Gossip, GossipEvent, GossipReceiver, GossipSender, JoinOptions}; +use iroh_gossip::{ + api::{Event, GossipReceiver, GossipSender, JoinOptions}, + net::Gossip, +}; use tokio::{ sync::mpsc, task::{AbortHandle, JoinSet}, @@ -43,15 +46,16 @@ impl GossipState { pub async fn join(&mut self, namespace: NamespaceId, bootstrap: Vec) -> Result<()> { match self.active.entry(namespace) { - hash_map::Entry::Occupied(entry) => { + hash_map::Entry::Occupied(mut entry) => { if !bootstrap.is_empty() { - entry.get().sender.join_peers(bootstrap).await?; + entry.get_mut().sender.join_peers(bootstrap).await?; } } hash_map::Entry::Vacant(entry) => { let sub = self .gossip - .subscribe_with_opts(namespace.into(), JoinOptions::with_bootstrap(bootstrap)); + .subscribe_with_opts(namespace.into(), JoinOptions::with_bootstrap(bootstrap)) + .await?; let (sender, stream) = sub.split(); let abort_handle = self.active_tasks.spawn( @@ -85,14 +89,14 @@ impl GossipState { self.progress().await } - pub async fn broadcast(&self, namespace: &NamespaceId, message: Bytes) { - if let Some(state) = self.active.get(namespace) { + pub async fn broadcast(&mut self, namespace: &NamespaceId, message: Bytes) { + if let Some(state) = self.active.get_mut(namespace) { state.sender.broadcast(message).await.ok(); } } - pub async fn broadcast_neighbors(&self, namespace: &NamespaceId, message: Bytes) { - if let Some(state) = self.active.get(namespace) { + pub async fn broadcast_neighbors(&mut self, namespace: &NamespaceId, message: Bytes) { + if let Some(state) = self.active.get_mut(namespace) { state.sender.broadcast_neighbors(message).await.ok(); } } @@ -142,15 +146,12 @@ async fn receive_loop( .await?; } while let Some(event) = recv.try_next().await? { - let event = match event { - Event::Gossip(event) => event, + match event { Event::Lagged => { debug!("gossip loop lagged - dropping gossip event"); continue; } - }; - match event { - GossipEvent::Received(msg) => { + Event::Received(msg) => { let op: Op = postcard::from_bytes(&msg.content)?; match op { Op::Put(entry) => { @@ -192,23 +193,16 @@ async fn receive_loop( } } } - GossipEvent::NeighborUp(peer) => { + Event::NeighborUp(peer) => { to_sync_actor .send(ToLiveActor::NeighborUp { namespace, peer }) .await?; } - GossipEvent::NeighborDown(peer) => { + Event::NeighborDown(peer) => { to_sync_actor .send(ToLiveActor::NeighborDown { namespace, peer }) .await?; } - GossipEvent::Joined(peers) => { - for peer in peers { - to_sync_actor - .send(ToLiveActor::NeighborUp { namespace, peer }) - .await?; - } - } } } Ok(()) diff --git a/src/engine/live.rs b/src/engine/live.rs index 31815bc..58dc285 100644 --- a/src/engine/live.rs +++ b/src/engine/live.rs @@ -10,9 +10,11 @@ use anyhow::{Context, Result}; use futures_lite::FutureExt; use iroh::{Endpoint, NodeAddr, NodeId, PublicKey}; use iroh_blobs::{ - downloader::{DownloadError, DownloadRequest, Downloader}, - get::Stats, - store::EntryStatus, + api::{ + blobs::BlobStatus, + downloader::{ContentDiscovery, DownloadRequest, Downloader, SplitStrategy}, + Store, + }, Hash, HashAndFormat, }; use iroh_gossip::net::Gossip; @@ -145,15 +147,15 @@ type SyncConnectRes = ( Result, ); type SyncAcceptRes = Result; -type DownloadRes = (NamespaceId, Hash, Result); +type DownloadRes = (NamespaceId, Hash, Result<(), anyhow::Error>); // Currently peers might double-sync in both directions. -pub struct LiveActor { +pub struct LiveActor { /// Receiver for actor messages. inbox: mpsc::Receiver, sync: SyncHandle, endpoint: Endpoint, - bao_store: B, + bao_store: Store, downloader: Downloader, replica_events_tx: async_channel::Sender, replica_events_rx: async_channel::Receiver, @@ -174,6 +176,8 @@ pub struct LiveActor { missing_hashes: HashSet, /// Content hashes queued in downloader. queued_hashes: QueuedHashes, + /// Nodes known to have a hash + hash_providers: ProviderNodes, /// Subscribers to actor events subscribers: SubscribersMap, @@ -182,14 +186,14 @@ pub struct LiveActor { state: NamespaceStates, metrics: Arc, } -impl LiveActor { +impl LiveActor { /// Create the live actor. #[allow(clippy::too_many_arguments)] pub fn new( sync: SyncHandle, endpoint: Endpoint, gossip: Gossip, - bao_store: B, + bao_store: Store, downloader: Downloader, inbox: mpsc::Receiver, sync_actor_tx: mpsc::Sender, @@ -214,6 +218,7 @@ impl LiveActor { state: Default::default(), missing_hashes: Default::default(), queued_hashes: Default::default(), + hash_providers: Default::default(), metrics, } } @@ -626,7 +631,7 @@ impl LiveActor { } } - async fn broadcast_neighbors(&self, namespace: NamespaceId, op: &Op) { + async fn broadcast_neighbors(&mut self, namespace: NamespaceId, op: &Op) { if !self.state.is_syncing(&namespace) { return; } @@ -648,7 +653,7 @@ impl LiveActor { &mut self, namespace: NamespaceId, hash: Hash, - res: Result, + res: Result<(), anyhow::Error>, ) { let completed_namespaces = self.queued_hashes.remove_hash(&hash); debug!(namespace=%namespace.fmt_short(), success=res.is_ok(), completed_namespaces=completed_namespaces.len(), "download ready"); @@ -750,17 +755,27 @@ impl LiveActor { node: PublicKey, only_if_missing: bool, ) { - let entry_status = self.bao_store.entry_status(&hash).await; - if matches!(entry_status, Ok(EntryStatus::Complete)) { + let entry_status = self.bao_store.blobs().status(hash).await; + if matches!(entry_status, Ok(BlobStatus::Complete { .. })) { self.missing_hashes.remove(&hash); return; } + self.hash_providers + .0 + .lock() + .expect("poisoned") + .entry(hash) + .or_default() + .insert(node); if self.queued_hashes.contains_hash(&hash) { self.queued_hashes.insert(hash, namespace); - self.downloader.nodes_have(hash, vec![node]).await; } else if !only_if_missing || self.missing_hashes.contains(&hash) { - let req = DownloadRequest::new(HashAndFormat::raw(hash), vec![node]); - let handle = self.downloader.queue(req).await; + let req = DownloadRequest::new( + HashAndFormat::raw(hash), + self.hash_providers.clone(), + SplitStrategy::None, + ); + let handle = self.downloader.download_with_opts(req); self.queued_hashes.insert(hash, namespace); self.missing_hashes.remove(&hash); @@ -882,6 +897,24 @@ struct QueuedHashes { by_namespace: HashMap>, } +#[derive(Debug, Clone, Default)] +struct ProviderNodes(Arc>>>); + +impl ContentDiscovery for ProviderNodes { + fn find_providers(&self, hash: HashAndFormat) -> n0_future::stream::Boxed { + let nodes = self + .0 + .lock() + .expect("poisoned") + .get(&hash.hash) + .into_iter() + .flatten() + .cloned() + .collect::>(); + Box::pin(n0_future::stream::iter(nodes)) + } +} + impl QueuedHashes { fn insert(&mut self, hash: Hash, namespace: NamespaceId) { self.by_hash.entry(hash).or_default().insert(namespace); diff --git a/src/keys.rs b/src/keys.rs index 2f83015..fec7bed 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -195,7 +195,7 @@ impl fmt::Display for NamespaceId { impl fmt::Debug for NamespaceSecret { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Namespace({})", self) + write!(f, "Namespace({self})") } } @@ -213,19 +213,19 @@ impl fmt::Debug for AuthorId { impl fmt::Debug for Author { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Author({})", self) + write!(f, "Author({self})") } } impl fmt::Debug for NamespacePublicKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "NamespacePublicKey({})", self) + write!(f, "NamespacePublicKey({self})") } } impl fmt::Debug for AuthorPublicKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "AuthorPublicKey({})", self) + write!(f, "AuthorPublicKey({self})") } } diff --git a/src/lib.rs b/src/lib.rs index 966ff3b..3789388 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,22 +36,14 @@ #![cfg_attr(iroh_docsrs, feature(doc_auto_cfg))] pub mod metrics; -#[cfg(feature = "net")] pub mod net; -#[cfg(feature = "engine")] pub mod protocol; -#[cfg(feature = "net")] mod ticket; -#[cfg(feature = "engine")] pub mod engine; -#[cfg(feature = "rpc")] -pub mod rpc; -#[cfg(feature = "cli")] -pub mod cli; - -pub mod actor; +mod actor; +pub mod api; pub mod store; pub mod sync; @@ -59,10 +51,7 @@ mod heads; mod keys; mod ranger; -#[cfg(feature = "net")] #[doc(inline)] pub use net::ALPN; -#[cfg(feature = "net")] -pub use self::ticket::DocTicket; -pub use self::{heads::*, keys::*, sync::*}; +pub use self::{heads::*, keys::*, sync::*, ticket::DocTicket}; diff --git a/src/protocol.rs b/src/protocol.rs index 2604432..74cc449 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -3,41 +3,24 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::Result; -use futures_lite::future::Boxed as BoxedFuture; -use iroh::{endpoint::Connection, protocol::ProtocolHandler}; -use iroh_blobs::net_protocol::{Blobs, ProtectCb}; +use iroh::{endpoint::Connection, protocol::ProtocolHandler, Endpoint}; +use iroh_blobs::api::Store as BlobsStore; use iroh_gossip::net::Gossip; use crate::{ - engine::{DefaultAuthorStorage, Engine}, + api::DocsApi, + engine::{DefaultAuthorStorage, Engine, ProtectCallbackHandler}, store::Store, }; -impl ProtocolHandler for Docs { - fn accept(&self, conn: Connection) -> BoxedFuture> { - let this = self.engine.clone(); - Box::pin(async move { this.handle_connection(conn).await }) - } - - fn shutdown(&self) -> BoxedFuture<()> { - let this = self.engine.clone(); - Box::pin(async move { - if let Err(err) = this.shutdown().await { - tracing::warn!("shutdown error: {:?}", err); - } - }) - } -} - /// Docs protocol. #[derive(Debug, Clone)] -pub struct Docs { - engine: Arc>, - #[cfg(feature = "rpc")] - pub(crate) rpc_handler: Arc>, +pub struct Docs { + engine: Arc, + api: DocsApi, } -impl Docs<()> { +impl Docs { /// Create a new [`Builder`] for the docs protocol, using in memory replica and author storage. pub fn memory() -> Builder { Builder::default() @@ -46,48 +29,46 @@ impl Docs<()> { /// Create a new [`Builder`] for the docs protocol, using a persistent replica and author storage /// in the given directory. pub fn persistent(path: PathBuf) -> Builder { - Builder { path: Some(path) } + Builder { + path: Some(path), + protect_cb: None, + } } -} -impl Docs { - /// Get an in memory client to interact with the docs engine. - #[cfg(feature = "rpc")] - pub fn client(&self) -> &crate::rpc::client::docs::MemClient { - &self - .rpc_handler - .get_or_init(|| crate::rpc::RpcHandler::new(self.engine.clone())) - .client + /// Creates a new [`Docs`] from an [`Engine`]. + pub fn new(engine: Engine) -> Self { + let engine = Arc::new(engine); + let api = DocsApi::spawn(engine.clone()); + Self { engine, api } } - /// Create a new docs protocol with the given engine. - /// - /// Note that usually you would use the [`Builder`] to create a new docs protocol. - pub fn new(engine: Engine) -> Self { - Self { - engine: Arc::new(engine), - #[cfg(feature = "rpc")] - rpc_handler: Default::default(), - } + /// Returns the API for this docs instance. + pub fn api(&self) -> &DocsApi { + &self.api } +} - /// Handle a docs request from the RPC server. - #[cfg(feature = "rpc")] - pub async fn handle_rpc_request< - C: quic_rpc::server::ChannelTypes, - >( - self, - msg: crate::rpc::proto::Request, - chan: quic_rpc::server::RpcChannel, - ) -> Result<(), quic_rpc::server::RpcServerError> { - crate::rpc::Handler(self.engine.clone()) - .handle_rpc_request(msg, chan) +impl std::ops::Deref for Docs { + type Target = DocsApi; + + fn deref(&self) -> &Self::Target { + &self.api + } +} + +impl ProtocolHandler for Docs { + async fn accept(&self, connection: Connection) -> Result<(), iroh::protocol::AcceptError> { + self.engine + .handle_connection(connection) .await + .map_err(|err| err.into_boxed_dyn_error())?; + Ok(()) } - /// Get the protect callback for the docs engine. - pub fn protect_cb(&self) -> ProtectCb { - self.engine.protect_cb() + async fn shutdown(&self) { + if let Err(err) = self.engine.shutdown().await { + tracing::warn!("shutdown error: {:?}", err); + } } } @@ -95,15 +76,25 @@ impl Docs { #[derive(Debug, Default)] pub struct Builder { path: Option, + protect_cb: Option, } impl Builder { - /// Build a [`Docs`] protocol given a [`Blobs`] and [`Gossip`] protocol. - pub async fn spawn( + /// Set the garbage collection protection handler for blobs. + /// + /// See [`ProtectCallbackHandler::new`] for details. + pub fn protect_handler(mut self, protect_handler: ProtectCallbackHandler) -> Self { + self.protect_cb = Some(protect_handler); + self + } + + /// Build a [`Docs`] protocol given a [`BlobsStore`] and [`Gossip`] protocol. + pub async fn spawn( self, - blobs: &Blobs, - gossip: &Gossip, - ) -> anyhow::Result> { + endpoint: Endpoint, + blobs: BlobsStore, + gossip: Gossip, + ) -> anyhow::Result { let replica_store = match self.path { Some(ref path) => Store::persistent(path.join("docs.redb"))?, None => Store::memory(), @@ -112,14 +103,15 @@ impl Builder { Some(ref path) => DefaultAuthorStorage::Persistent(path.join("default-author")), None => DefaultAuthorStorage::Mem, }; + let downloader = blobs.downloader(&endpoint); let engine = Engine::spawn( - blobs.endpoint().clone(), - gossip.clone(), + endpoint, + gossip, replica_store, - blobs.store().clone(), - blobs.downloader().clone(), + blobs, + downloader, author_store, - blobs.rt().clone(), + self.protect_cb, ) .await?; Ok(Docs::new(engine)) diff --git a/src/ranger.rs b/src/ranger.rs index 3738511..ca79cee 100644 --- a/src/ranger.rs +++ b/src/ranger.rs @@ -1,8 +1,9 @@ //! Implementation of Set Reconcilliation based on //! "Range-Based Set Reconciliation" by Aljoscha Meyer. -use std::{cmp::Ordering, fmt::Debug}; +use std::{cmp::Ordering, fmt::Debug, pin::Pin}; +use n0_future::StreamExt; use serde::{Deserialize, Serialize}; use crate::ContentStatus; @@ -316,7 +317,7 @@ pub trait Store: Sized { /// /// `content_status_cb` is called for each outgoing entry about to be sent to the remote. /// It must return a [`ContentStatus`], which will be sent to the remote with the entry. - fn process_message( + async fn process_message( &mut self, config: &SyncConfig, message: Message, @@ -327,7 +328,10 @@ pub trait Store: Sized { where F: Fn(&Self, &E, ContentStatus) -> bool, F2: FnMut(&Self, E, ContentStatus), - F3: Fn(&Self, &E) -> ContentStatus, + F3: for<'a> Fn( + &'a E, + ) + -> Pin + Send + 'a>>, { let mut out = Vec::new(); @@ -376,13 +380,11 @@ pub trait Store: Sized { }) .collect::, _>>()?; // add the content status in a second pass - items - .into_iter() - .map(|entry| { - let content_status = content_status_cb(self, &entry); - (entry, content_status) - }) - .collect() + let values = items.into_iter().map(|entry| async { + let content_status = content_status_cb(&entry).await; + (entry, content_status) + }); + n0_future::FuturesOrdered::from_iter(values).collect().await }) }; @@ -422,13 +424,11 @@ pub trait Store: Sized { let values = self .get_range(range.clone())? .collect::, _>>()?; - let values = values - .into_iter() - .map(|entry| { - let content_status = content_status_cb(self, &entry); - (entry, content_status) - }) - .collect(); + let values = values.into_iter().map(|entry| async { + let content_status = content_status_cb(&entry).await; + (entry, content_status) + }); + let values = n0_future::FuturesOrdered::from_iter(values).collect().await; out.push(MessagePart::RangeItem(RangeItem { range, values, @@ -519,15 +519,15 @@ pub trait Store: Sized { fingerprint, })); } else { - let values = chunk - .into_iter() - .map(|entry| { - entry.map(|entry| { - let content_status = content_status_cb(self, &entry); - (entry, content_status) - }) - }) - .collect::>()?; + // let content_status_cb = content_status_cb.clone(); + let values = chunk.into_iter().filter_map(|entry| match entry { + Err(_err) => None, + Ok(entry) => Some(async { + let content_status = content_status_cb(&entry).await; + (entry, content_status) + }), + }); + let values = n0_future::FuturesOrdered::from_iter(values).collect().await; out.push(MessagePart::RangeItem(RangeItem { range, values, @@ -903,8 +903,8 @@ mod tests { } } - #[test] - fn test_paper_1() { + #[tokio::test] + async fn test_paper_1() { let alice_set = [("ape", 1), ("eel", 1), ("fox", 1), ("gnu", 1)]; let bob_set = [ ("bee", 1), @@ -915,7 +915,7 @@ mod tests { ("hog", 1), ]; - let res = sync(&alice_set, &bob_set); + let res = sync(&alice_set, &bob_set).await; res.print_messages(); assert_eq!(res.alice_to_bob.len(), 3, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 2, "B -> A message count"); @@ -940,8 +940,8 @@ mod tests { assert!(res.bob_to_alice[1].parts[1].is_range_item()); } - #[test] - fn test_paper_2() { + #[tokio::test] + async fn test_paper_2() { let alice_set = [ ("ape", 1), ("bee", 1), @@ -962,13 +962,13 @@ mod tests { ("hog", 1), ]; - let res = sync(&alice_set, &bob_set); + let res = sync(&alice_set, &bob_set).await; assert_eq!(res.alice_to_bob.len(), 3, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 2, "B -> A message count"); } - #[test] - fn test_paper_3() { + #[tokio::test] + async fn test_paper_3() { let alice_set = [ ("ape", 1), ("bee", 1), @@ -981,63 +981,63 @@ mod tests { ]; let bob_set = [("ape", 1), ("cat", 1), ("eel", 1), ("gnu", 1)]; - let res = sync(&alice_set, &bob_set); + let res = sync(&alice_set, &bob_set).await; assert_eq!(res.alice_to_bob.len(), 3, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 2, "B -> A message count"); } - #[test] - fn test_limits() { + #[tokio::test] + async fn test_limits() { let alice_set = [("ape", 1), ("bee", 1), ("cat", 1)]; let bob_set = [("ape", 1), ("cat", 1), ("doe", 1)]; - let res = sync(&alice_set, &bob_set); + let res = sync(&alice_set, &bob_set).await; assert_eq!(res.alice_to_bob.len(), 2, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 2, "B -> A message count"); } - #[test] - fn test_prefixes_simple() { + #[tokio::test] + async fn test_prefixes_simple() { let alice_set = [("/foo/bar", 1), ("/foo/baz", 1), ("/foo/cat", 1)]; let bob_set = [("/foo/bar", 1), ("/alice/bar", 1), ("/alice/baz", 1)]; - let res = sync(&alice_set, &bob_set); + let res = sync(&alice_set, &bob_set).await; assert_eq!(res.alice_to_bob.len(), 2, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 2, "B -> A message count"); } - #[test] - fn test_prefixes_empty_alice() { + #[tokio::test] + async fn test_prefixes_empty_alice() { let alice_set = []; let bob_set = [("/foo/bar", 1), ("/alice/bar", 1), ("/alice/baz", 1)]; - let res = sync(&alice_set, &bob_set); + let res = sync(&alice_set, &bob_set).await; assert_eq!(res.alice_to_bob.len(), 1, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 1, "B -> A message count"); } - #[test] - fn test_prefixes_empty_bob() { + #[tokio::test] + async fn test_prefixes_empty_bob() { let alice_set = [("/foo/bar", 1), ("/foo/baz", 1), ("/foo/cat", 1)]; let bob_set = []; - let res = sync(&alice_set, &bob_set); + let res = sync(&alice_set, &bob_set).await; assert_eq!(res.alice_to_bob.len(), 2, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 1, "B -> A message count"); } - #[test] - fn test_equal_key_higher_value() { + #[tokio::test] + async fn test_equal_key_higher_value() { let alice_set = [("foo", 2)]; let bob_set = [("foo", 1)]; - let res = sync(&alice_set, &bob_set); + let res = sync(&alice_set, &bob_set).await; assert_eq!(res.alice_to_bob.len(), 2, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 1, "B -> A message count"); } - #[test] - fn test_multikey() { + #[tokio::test] + async fn test_multikey() { /// Uses the blanket impl of [`RangeKey]` for `T: AsRef<[u8]>` in this module. #[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord)] struct Multikey { @@ -1089,7 +1089,7 @@ mod tests { ]; // No limit - let mut res = sync(&alice_set, &bob_set); + let mut res = sync(&alice_set, &bob_set).await; assert_eq!(res.alice_to_bob.len(), 2, "A -> B message count"); assert_eq!(res.bob_to_alice.len(), 2, "B -> A message count"); res.assert_alice_set( @@ -1120,8 +1120,8 @@ mod tests { // This tests two things: // 1) validate cb returning false leads to no changes on both sides after sync // 2) validate cb receives expected entries - #[test] - fn test_validate_cb() { + #[tokio::test] + async fn test_validate_cb() { let alice_set = [("alice1", 1), ("alice2", 2)]; let bob_set = [("bob1", 3), ("bob2", 4), ("bob3", 5)]; let alice_validate_set = Rc::new(RefCell::new(vec![])); @@ -1153,7 +1153,7 @@ mod tests { } // run sync with a validate callback returning false, so no new entries are stored on either side - let mut res = sync_exchange_messages(alice, bob, &validate_alice, &validate_bob, 100); + let mut res = sync_exchange_messages(alice, bob, &validate_alice, &validate_bob, 100).await; res.assert_alice_set("unchanged", &alice_set); res.assert_bob_set("unchanged", &bob_set); @@ -1203,12 +1203,7 @@ mod tests { e.key() ); } - assert_eq!( - expected.len(), - self.alice.len().unwrap(), - "{}: (alice)", - ctx - ); + assert_eq!(expected.len(), self.alice.len().unwrap(), "{ctx}: (alice)"); } fn assert_bob_set(&mut self, ctx: &str, expected: &[(K, V)]) { @@ -1218,12 +1213,10 @@ mod tests { assert_eq!( self.bob.get(e.key()).unwrap().as_ref(), Some(e), - "{}: (bob) missing key {:?}", - ctx, - e + "{ctx}: (bob) missing key {e:?}" ); } - assert_eq!(expected.len(), self.bob.len().unwrap(), "{}: (bob)", ctx); + assert_eq!(expected.len(), self.bob.len().unwrap(), "{ctx}: (bob)"); } } @@ -1257,7 +1250,7 @@ mod tests { type ValidateCb = Box, &(K, V), ContentStatus) -> bool>; - fn sync(alice_set: &[(K, V)], bob_set: &[(K, V)]) -> SyncResult + async fn sync(alice_set: &[(K, V)], bob_set: &[(K, V)]) -> SyncResult where K: RangeKey + Default, V: RangeValue, @@ -1265,6 +1258,7 @@ mod tests { let alice_validate_cb: ValidateCb = Box::new(|_, _, _| true); let bob_validate_cb: ValidateCb = Box::new(|_, _, _| true); sync_with_validate_cb_and_assert(alice_set, bob_set, &alice_validate_cb, &bob_validate_cb) + .await } fn insert_if_larger(map: &mut BTreeMap, key: K, value: V) { @@ -1287,7 +1281,7 @@ mod tests { } } - fn sync_with_validate_cb_and_assert( + async fn sync_with_validate_cb_and_assert( alice_set: &[(K, V)], bob_set: &[(K, V)], alice_validate_cb: F1, @@ -1330,7 +1324,8 @@ mod tests { expected_set.into_iter().collect::>() }; - let mut res = sync_exchange_messages(alice, bob, alice_validate_cb, bob_validate_cb, 100); + let mut res = + sync_exchange_messages(alice, bob, alice_validate_cb, bob_validate_cb, 100).await; let alice_now: Vec<_> = res.alice.all().unwrap().collect::>().unwrap(); if alice_now != expected_set { @@ -1360,8 +1355,7 @@ mod tests { for (e, _) in values { assert!( alice_sent.insert(e.key(), e).is_none(), - "alice: duplicate {:?}", - e + "alice: duplicate {e:?}" ); } } @@ -1375,8 +1369,7 @@ mod tests { for (e, _) in values { assert!( bob_sent.insert(e.key(), e).is_none(), - "bob: duplicate {:?}", - e + "bob: duplicate {e:?}" ); } } @@ -1386,7 +1379,7 @@ mod tests { res } - fn sync_exchange_messages( + async fn sync_exchange_messages( mut alice: SimpleStore, mut bob: SimpleStore, alice_validate_cb: F1, @@ -1416,8 +1409,9 @@ mod tests { msg, &bob_validate_cb, |_, _, _| (), - |_, _| ContentStatus::Complete, + |_| Box::pin(async move { ContentStatus::Complete }), ) + .await .unwrap() { bob_to_alice.push(msg.clone()); @@ -1427,8 +1421,9 @@ mod tests { msg, &alice_validate_cb, |_, _, _| (), - |_, _| ContentStatus::Complete, + |_| Box::pin(async move { ContentStatus::Complete }), ) + .await .unwrap(); } } @@ -1551,32 +1546,32 @@ mod tests { mk_test_set(values).into_iter().collect() } - #[test] - fn simple_store_sync_1() { + #[tokio::test] + async fn simple_store_sync_1() { let alice = mk_test_vec(["3"]); let bob = mk_test_vec(["2", "3", "4", "5", "6", "7", "8"]); - let _res = sync(&alice, &bob); + let _res = sync(&alice, &bob).await; } - #[test] - fn simple_store_sync_x() { + #[tokio::test] + async fn simple_store_sync_x() { let alice = mk_test_vec(["1", "3"]); let bob = mk_test_vec(["2"]); - let _res = sync(&alice, &bob); + let _res = sync(&alice, &bob).await; } - #[test] - fn simple_store_sync_2() { + #[tokio::test] + async fn simple_store_sync_2() { let alice = mk_test_vec(["1", "3"]); let bob = mk_test_vec(["0", "2", "3"]); - let _res = sync(&alice, &bob); + let _res = sync(&alice, &bob).await; } - #[test] - fn simple_store_sync_3() { + #[tokio::test] + async fn simple_store_sync_3() { let alice = mk_test_vec(["8", "9"]); let bob = mk_test_vec(["1", "2", "3"]); - let _res = sync(&alice, &bob); + let _res = sync(&alice, &bob).await; } #[proptest] @@ -1584,7 +1579,13 @@ mod tests { #[strategy(test_vec_string_unit())] alice: Vec<(String, ())>, #[strategy(test_vec_string_unit())] bob: Vec<(String, ())>, ) { - let _res = sync(&alice, &bob); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async move { + let _res = sync(&alice, &bob).await; + }); } #[proptest] @@ -1592,7 +1593,13 @@ mod tests { #[strategy(test_vec_string_u8())] alice: Vec<(String, u8)>, #[strategy(test_vec_string_u8())] bob: Vec<(String, u8)>, ) { - let _res = sync(&alice, &bob); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async move { + let _res = sync(&alice, &bob).await; + }); } /// A generic fn to make a test for the get_range fn of a store. diff --git a/src/rpc.rs b/src/rpc.rs deleted file mode 100644 index 1aaff7f..0000000 --- a/src/rpc.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Quic RPC implementation for docs. - -use std::{ops::Deref, sync::Arc}; - -use iroh::NodeAddr; -use proto::{Request, RpcService}; -use quic_rpc::{ - server::{ChannelTypes, RpcChannel}, - RpcClient, RpcServer, -}; -use serde::{Deserialize, Serialize}; -use tokio_util::task::AbortOnDropHandle; - -use crate::engine::Engine; - -pub mod client; -pub mod proto; - -mod docs_handle_request; - -type RpcError = serde_error::Error; -type RpcResult = std::result::Result; - -#[derive(Debug, Clone)] -pub(crate) struct Handler(pub(crate) Arc>); - -impl Deref for Handler { - type Target = Engine; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Handler { - /// Handle a docs request from the RPC server. - pub async fn handle_rpc_request>( - self, - msg: Request, - chan: RpcChannel, - ) -> Result<(), quic_rpc::server::RpcServerError> { - use Request::*; - let this = self; - match msg { - Open(msg) => chan.rpc(msg, this, Self::doc_open).await, - Close(msg) => chan.rpc(msg, this, Self::doc_close).await, - Status(msg) => chan.rpc(msg, this, Self::doc_status).await, - List(msg) => chan.server_streaming(msg, this, Self::doc_list).await, - Create(msg) => chan.rpc(msg, this, Self::doc_create).await, - Drop(msg) => chan.rpc(msg, this, Self::doc_drop).await, - Import(msg) => chan.rpc(msg, this, Self::doc_import).await, - Set(msg) => chan.rpc(msg, this, Self::doc_set).await, - ImportFile(msg) => { - chan.server_streaming(msg, this, Self::doc_import_file) - .await - } - ExportFile(msg) => { - chan.server_streaming(msg, this, Self::doc_export_file) - .await - } - Del(msg) => chan.rpc(msg, this, Self::doc_del).await, - SetHash(msg) => chan.rpc(msg, this, Self::doc_set_hash).await, - Get(msg) => chan.server_streaming(msg, this, Self::doc_get_many).await, - GetExact(msg) => chan.rpc(msg, this, Self::doc_get_exact).await, - StartSync(msg) => chan.rpc(msg, this, Self::doc_start_sync).await, - Leave(msg) => chan.rpc(msg, this, Self::doc_leave).await, - Share(msg) => chan.rpc(msg, this, Self::doc_share).await, - Subscribe(msg) => { - chan.try_server_streaming(msg, this, Self::doc_subscribe) - .await - } - SetDownloadPolicy(msg) => chan.rpc(msg, this, Self::doc_set_download_policy).await, - GetDownloadPolicy(msg) => chan.rpc(msg, this, Self::doc_get_download_policy).await, - GetSyncPeers(msg) => chan.rpc(msg, this, Self::doc_get_sync_peers).await, - - AuthorList(msg) => chan.server_streaming(msg, this, Self::author_list).await, - AuthorCreate(msg) => chan.rpc(msg, this, Self::author_create).await, - AuthorImport(msg) => chan.rpc(msg, this, Self::author_import).await, - AuthorExport(msg) => chan.rpc(msg, this, Self::author_export).await, - AuthorDelete(msg) => chan.rpc(msg, this, Self::author_delete).await, - AuthorGetDefault(msg) => chan.rpc(msg, this, Self::author_default).await, - AuthorSetDefault(msg) => chan.rpc(msg, this, Self::author_set_default).await, - } - } -} - -#[derive(Debug)] -pub(crate) struct RpcHandler { - /// Client to hand out - pub(crate) client: client::docs::MemClient, - /// Handler task - _handler: AbortOnDropHandle<()>, -} - -impl RpcHandler { - pub fn new(engine: Arc>) -> Self { - let engine = Handler(engine); - let (listener, connector) = quic_rpc::transport::flume::channel(1); - let listener = RpcServer::new(listener); - let client = client::docs::MemClient::new(RpcClient::new(connector)); - let _handler = listener - .spawn_accept_loop(move |req, chan| engine.clone().handle_rpc_request(req, chan)); - Self { client, _handler } - } -} - -/// Options to configure what is included in a [`NodeAddr`]. -#[derive( - Copy, - Clone, - PartialEq, - Eq, - Default, - Debug, - derive_more::Display, - derive_more::FromStr, - Serialize, - Deserialize, -)] -pub enum AddrInfoOptions { - /// Only the Node ID is added. - /// - /// This usually means that iroh-dns discovery is used to find address information. - #[default] - Id, - /// Includes the Node ID and both the relay URL, and the direct addresses. - RelayAndAddresses, - /// Includes the Node ID and the relay URL. - Relay, - /// Includes the Node ID and the direct addresses. - Addresses, -} - -impl AddrInfoOptions { - /// Apply the options to the given address. - pub fn apply( - &self, - NodeAddr { - node_id, - relay_url, - direct_addresses, - }: &NodeAddr, - ) -> NodeAddr { - match self { - Self::Id => NodeAddr { - node_id: *node_id, - relay_url: None, - direct_addresses: Default::default(), - }, - Self::Relay => NodeAddr { - node_id: *node_id, - relay_url: relay_url.clone(), - direct_addresses: Default::default(), - }, - Self::Addresses => NodeAddr { - node_id: *node_id, - relay_url: None, - direct_addresses: direct_addresses.clone(), - }, - Self::RelayAndAddresses => NodeAddr { - node_id: *node_id, - relay_url: relay_url.clone(), - direct_addresses: direct_addresses.clone(), - }, - } - } -} diff --git a/src/rpc/client.rs b/src/rpc/client.rs deleted file mode 100644 index 90a0ad7..0000000 --- a/src/rpc/client.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! RPC Client for docs and authors -use anyhow::Result; -use futures_util::{Stream, StreamExt}; - -pub mod authors; -pub mod docs; - -fn flatten( - s: impl Stream, E2>>, -) -> impl Stream> -where - E1: std::error::Error + Send + Sync + 'static, - E2: std::error::Error + Send + Sync + 'static, -{ - s.map(|res| match res { - Ok(Ok(res)) => Ok(res), - Ok(Err(err)) => Err(err.into()), - Err(err) => Err(err.into()), - }) -} diff --git a/src/rpc/client/authors.rs b/src/rpc/client/authors.rs deleted file mode 100644 index 18a154f..0000000 --- a/src/rpc/client/authors.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! API for document management. -//! -//! The main entry point is the [`Client`]. - -use anyhow::Result; -use futures_lite::{Stream, StreamExt}; -use quic_rpc::{client::BoxedConnector, Connector}; - -use super::flatten; -#[doc(inline)] -pub use crate::engine::{Origin, SyncEvent, SyncReason}; -use crate::{ - rpc::proto::{ - AuthorCreateRequest, AuthorDeleteRequest, AuthorExportRequest, AuthorGetDefaultRequest, - AuthorImportRequest, AuthorListRequest, AuthorSetDefaultRequest, RpcService, - }, - Author, AuthorId, -}; - -/// Iroh docs client. -#[derive(Debug, Clone)] -#[repr(transparent)] -pub struct Client> { - pub(super) rpc: quic_rpc::RpcClient, -} - -impl> Client { - /// Creates a new docs client. - pub fn new(rpc: quic_rpc::RpcClient) -> Self { - Self { rpc } - } - - /// Creates a new document author. - /// - /// You likely want to save the returned [`AuthorId`] somewhere so that you can use this author - /// again. - /// - /// If you need only a single author, use [`Self::default`]. - pub async fn create(&self) -> Result { - let res = self.rpc.rpc(AuthorCreateRequest).await??; - Ok(res.author_id) - } - - /// Returns the default document author of this node. - /// - /// On persistent nodes, the author is created on first start and its public key is saved - /// in the data directory. - /// - /// The default author can be set with [`Self::set_default`]. - pub async fn default(&self) -> Result { - let res = self.rpc.rpc(AuthorGetDefaultRequest).await??; - Ok(res.author_id) - } - - /// Sets the node-wide default author. - /// - /// If the author does not exist, an error is returned. - /// - /// On a persistent node, the author id will be saved to a file in the data directory and - /// reloaded after a restart. - pub async fn set_default(&self, author_id: AuthorId) -> Result<()> { - self.rpc - .rpc(AuthorSetDefaultRequest { author_id }) - .await??; - Ok(()) - } - - /// Lists document authors for which we have a secret key. - /// - /// It's only possible to create writes from authors that we have the secret key of. - pub async fn list(&self) -> Result>> { - let stream = self.rpc.server_streaming(AuthorListRequest {}).await?; - Ok(flatten(stream).map(|res| res.map(|res| res.author_id))) - } - - /// Exports the given author. - /// - /// Warning: The [`Author`] struct contains sensitive data. - pub async fn export(&self, author: AuthorId) -> Result> { - let res = self.rpc.rpc(AuthorExportRequest { author }).await??; - Ok(res.author) - } - - /// Imports the given author. - /// - /// Warning: The [`Author`] struct contains sensitive data. - pub async fn import(&self, author: Author) -> Result<()> { - self.rpc.rpc(AuthorImportRequest { author }).await??; - Ok(()) - } - - /// Deletes the given author by id. - /// - /// Warning: This permanently removes this author. - /// - /// Returns an error if attempting to delete the default author. - pub async fn delete(&self, author: AuthorId) -> Result<()> { - self.rpc.rpc(AuthorDeleteRequest { author }).await??; - Ok(()) - } -} diff --git a/src/rpc/client/docs.rs b/src/rpc/client/docs.rs deleted file mode 100644 index a0b44ea..0000000 --- a/src/rpc/client/docs.rs +++ /dev/null @@ -1,638 +0,0 @@ -//! API for document management. -//! -//! The main entry point is the [`Client`]. -use std::{ - path::{Path, PathBuf}, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use anyhow::{anyhow, Context as _, Result}; -use bytes::Bytes; -use derive_more::{Display, FromStr}; -use futures_lite::{Stream, StreamExt}; -use iroh::NodeAddr; -use iroh_blobs::{export::ExportProgress, store::ExportMode, Hash}; -use portable_atomic::{AtomicBool, Ordering}; -use quic_rpc::{ - client::BoxedConnector, message::RpcMsg, transport::flume::FlumeConnector, Connector, -}; -use serde::{Deserialize, Serialize}; - -use super::{authors, flatten}; -use crate::{ - actor::OpenState, - rpc::{ - proto::{ - CloseRequest, CreateRequest, DelRequest, DelResponse, DocListRequest, - DocSubscribeRequest, DropRequest, ExportFileRequest, GetDownloadPolicyRequest, - GetExactRequest, GetManyRequest, GetSyncPeersRequest, ImportFileRequest, ImportRequest, - LeaveRequest, OpenRequest, RpcService, SetDownloadPolicyRequest, SetHashRequest, - SetRequest, ShareRequest, StartSyncRequest, StatusRequest, - }, - AddrInfoOptions, - }, - store::{DownloadPolicy, Query}, - AuthorId, Capability, CapabilityKind, DocTicket, NamespaceId, PeerIdBytes, -}; -#[doc(inline)] -pub use crate::{ - engine::{LiveEvent, Origin, SyncEvent, SyncReason}, - Entry, -}; - -/// Type alias for a memory-backed client. -pub type MemClient = - Client>; - -/// Iroh docs client. -#[derive(Debug, Clone)] -#[repr(transparent)] -pub struct Client> { - pub(super) rpc: quic_rpc::RpcClient, -} - -impl> Client { - /// Creates a new docs client. - pub fn new(rpc: quic_rpc::RpcClient) -> Self { - Self { rpc } - } - - /// Returns an authors client. - pub fn authors(&self) -> authors::Client { - authors::Client::new(self.rpc.clone()) - } - - /// Creates a client. - pub async fn create(&self) -> Result> { - let res = self.rpc.rpc(CreateRequest {}).await??; - let doc = Doc::new(self.rpc.clone(), res.id); - Ok(doc) - } - - /// Deletes a document from the local node. - /// - /// This is a destructive operation. Both the document secret key and all entries in the - /// document will be permanently deleted from the node's storage. Content blobs will be deleted - /// through garbage collection unless they are referenced from another document or tag. - pub async fn drop_doc(&self, doc_id: NamespaceId) -> Result<()> { - self.rpc.rpc(DropRequest { doc_id }).await??; - Ok(()) - } - - /// Imports a document from a namespace capability. - /// - /// This does not start sync automatically. Use [`Doc::start_sync`] to start sync. - pub async fn import_namespace(&self, capability: Capability) -> Result> { - let res = self.rpc.rpc(ImportRequest { capability }).await??; - let doc = Doc::new(self.rpc.clone(), res.doc_id); - Ok(doc) - } - - /// Imports a document from a ticket and joins all peers in the ticket. - pub async fn import(&self, ticket: DocTicket) -> Result> { - let DocTicket { capability, nodes } = ticket; - let doc = self.import_namespace(capability).await?; - doc.start_sync(nodes).await?; - Ok(doc) - } - - /// Imports a document from a ticket, creates a subscription stream and joins all peers in the ticket. - /// - /// Returns the [`Doc`] and a [`Stream`] of [`LiveEvent`]s. - /// - /// The subscription stream is created before the sync is started, so the first call to this - /// method after starting the node is guaranteed to not miss any sync events. - pub async fn import_and_subscribe( - &self, - ticket: DocTicket, - ) -> Result<(Doc, impl Stream>)> { - let DocTicket { capability, nodes } = ticket; - let res = self.rpc.rpc(ImportRequest { capability }).await??; - let doc = Doc::new(self.rpc.clone(), res.doc_id); - let events = doc.subscribe().await?; - doc.start_sync(nodes).await?; - Ok((doc, events)) - } - - /// Lists all documents. - pub async fn list(&self) -> Result>> { - let stream = self.rpc.server_streaming(DocListRequest {}).await?; - Ok(flatten(stream).map(|res| res.map(|res| (res.id, res.capability)))) - } - - /// Returns a [`Doc`] client for a single document. - /// - /// Returns None if the document cannot be found. - pub async fn open(&self, id: NamespaceId) -> Result>> { - self.rpc.rpc(OpenRequest { doc_id: id }).await??; - let doc = Doc::new(self.rpc.clone(), id); - Ok(Some(doc)) - } -} - -/// Document handle -#[derive(Debug, Clone)] -pub struct Doc = BoxedConnector>(Arc>) -where - C: quic_rpc::Connector; - -impl> PartialEq for Doc { - fn eq(&self, other: &Self) -> bool { - self.0.id == other.0.id - } -} - -impl> Eq for Doc {} - -#[derive(Debug)] -struct DocInner = BoxedConnector> { - id: NamespaceId, - rpc: quic_rpc::RpcClient, - closed: AtomicBool, - rt: tokio::runtime::Handle, -} - -impl Drop for DocInner -where - C: quic_rpc::Connector, -{ - fn drop(&mut self) { - let doc_id = self.id; - let rpc = self.rpc.clone(); - if !self.closed.swap(true, Ordering::Relaxed) { - self.rt.spawn(async move { - rpc.rpc(CloseRequest { doc_id }).await.ok(); - }); - } - } -} - -impl> Doc { - fn new(rpc: quic_rpc::RpcClient, id: NamespaceId) -> Self { - Self(Arc::new(DocInner { - rpc, - id, - closed: AtomicBool::new(false), - rt: tokio::runtime::Handle::current(), - })) - } - - async fn rpc(&self, msg: M) -> Result - where - M: RpcMsg, - { - let res = self.0.rpc.rpc(msg).await?; - Ok(res) - } - - /// Returns the document id of this doc. - pub fn id(&self) -> NamespaceId { - self.0.id - } - - /// Closes the document. - pub async fn close(&self) -> Result<()> { - if !self.0.closed.swap(true, Ordering::Relaxed) { - self.rpc(CloseRequest { doc_id: self.id() }).await??; - } - Ok(()) - } - - fn ensure_open(&self) -> Result<()> { - if self.0.closed.load(Ordering::Relaxed) { - Err(anyhow!("document is closed")) - } else { - Ok(()) - } - } - - /// Sets the content of a key to a byte array. - pub async fn set_bytes( - &self, - author_id: AuthorId, - key: impl Into, - value: impl Into, - ) -> Result { - self.ensure_open()?; - let res = self - .rpc(SetRequest { - doc_id: self.id(), - author_id, - key: key.into(), - value: value.into(), - }) - .await??; - Ok(res.entry.content_hash()) - } - - /// Sets an entries on the doc via its key, hash, and size. - pub async fn set_hash( - &self, - author_id: AuthorId, - key: impl Into, - hash: Hash, - size: u64, - ) -> Result<()> { - self.ensure_open()?; - self.rpc(SetHashRequest { - doc_id: self.id(), - author_id, - key: key.into(), - hash, - size, - }) - .await??; - Ok(()) - } - - /// Adds an entry from an absolute file path - pub async fn import_file( - &self, - author: AuthorId, - key: Bytes, - path: impl AsRef, - in_place: bool, - ) -> Result { - self.ensure_open()?; - let stream = self - .0 - .rpc - .server_streaming(ImportFileRequest { - doc_id: self.id(), - author_id: author, - path: path.as_ref().into(), - key, - in_place, - }) - .await?; - Ok(ImportFileProgress::new(stream)) - } - - /// Exports an entry as a file to a given absolute path. - pub async fn export_file( - &self, - entry: Entry, - path: impl AsRef, - mode: ExportMode, - ) -> Result { - self.ensure_open()?; - let stream = self - .0 - .rpc - .server_streaming(ExportFileRequest { - entry, - path: path.as_ref().into(), - mode, - }) - .await?; - Ok(ExportFileProgress::new(stream)) - } - - /// Deletes entries that match the given `author` and key `prefix`. - /// - /// This inserts an empty entry with the key set to `prefix`, effectively clearing all other - /// entries whose key starts with or is equal to the given `prefix`. - /// - /// Returns the number of entries deleted. - pub async fn del(&self, author_id: AuthorId, prefix: impl Into) -> Result { - self.ensure_open()?; - let res = self - .rpc(DelRequest { - doc_id: self.id(), - author_id, - prefix: prefix.into(), - }) - .await??; - let DelResponse { removed } = res; - Ok(removed) - } - - /// Returns an entry for a key and author. - /// - /// Optionally also returns the entry unless it is empty (i.e. a deletion marker). - pub async fn get_exact( - &self, - author: AuthorId, - key: impl AsRef<[u8]>, - include_empty: bool, - ) -> Result> { - self.ensure_open()?; - let res = self - .rpc(GetExactRequest { - author, - key: key.as_ref().to_vec().into(), - doc_id: self.id(), - include_empty, - }) - .await??; - Ok(res.entry.map(|entry| entry.into())) - } - - /// Returns all entries matching the query. - pub async fn get_many( - &self, - query: impl Into, - ) -> Result>> { - self.ensure_open()?; - let stream = self - .0 - .rpc - .server_streaming(GetManyRequest { - doc_id: self.id(), - query: query.into(), - }) - .await?; - Ok(flatten(stream).map(|res| res.map(|res| res.entry.into()))) - } - - /// Returns a single entry. - pub async fn get_one(&self, query: impl Into) -> Result> { - self.get_many(query).await?.next().await.transpose() - } - - /// Shares this document with peers over a ticket. - pub async fn share( - &self, - mode: ShareMode, - addr_options: AddrInfoOptions, - ) -> anyhow::Result { - self.ensure_open()?; - let res = self - .rpc(ShareRequest { - doc_id: self.id(), - mode, - addr_options, - }) - .await??; - Ok(res.0) - } - - /// Starts to sync this document with a list of peers. - pub async fn start_sync(&self, peers: Vec) -> Result<()> { - self.ensure_open()?; - let _res = self - .rpc(StartSyncRequest { - doc_id: self.id(), - peers, - }) - .await??; - Ok(()) - } - - /// Stops the live sync for this document. - pub async fn leave(&self) -> Result<()> { - self.ensure_open()?; - let _res = self.rpc(LeaveRequest { doc_id: self.id() }).await??; - Ok(()) - } - - /// Subscribes to events for this document. - pub async fn subscribe(&self) -> anyhow::Result>> { - self.ensure_open()?; - let stream = self - .0 - .rpc - .try_server_streaming(DocSubscribeRequest { doc_id: self.id() }) - .await?; - Ok(stream.map(|res| match res { - Ok(res) => Ok(res.event), - Err(err) => Err(err.into()), - })) - } - - /// Returns status info for this document - pub async fn status(&self) -> anyhow::Result { - self.ensure_open()?; - let res = self.rpc(StatusRequest { doc_id: self.id() }).await??; - Ok(res.status) - } - - /// Sets the download policy for this document - pub async fn set_download_policy(&self, policy: DownloadPolicy) -> Result<()> { - self.rpc(SetDownloadPolicyRequest { - doc_id: self.id(), - policy, - }) - .await??; - Ok(()) - } - - /// Returns the download policy for this document - pub async fn get_download_policy(&self) -> Result { - let res = self - .rpc(GetDownloadPolicyRequest { doc_id: self.id() }) - .await??; - Ok(res.policy) - } - - /// Returns sync peers for this document - pub async fn get_sync_peers(&self) -> Result>> { - let res = self - .rpc(GetSyncPeersRequest { doc_id: self.id() }) - .await??; - Ok(res.peers) - } -} - -impl<'a, C> From<&'a Doc> for &'a quic_rpc::RpcClient -where - C: quic_rpc::Connector, -{ - fn from(doc: &'a Doc) -> &'a quic_rpc::RpcClient { - &doc.0.rpc - } -} - -/// Progress messages for an doc import operation -/// -/// An import operation involves computing the outboard of a file, and then -/// either copying or moving the file into the database, then setting the author, hash, size, and tag of that -/// file as an entry in the doc. -#[derive(Debug, Serialize, Deserialize)] -pub enum ImportProgress { - /// An item was found with name `name`, from now on referred to via `id`. - Found { - /// A new unique id for this entry. - id: u64, - /// The name of the entry. - name: String, - /// The size of the entry in bytes. - size: u64, - }, - /// We got progress ingesting item `id`. - Progress { - /// The unique id of the entry. - id: u64, - /// The offset of the progress, in bytes. - offset: u64, - }, - /// We are done adding `id` to the data store and the hash is `hash`. - IngestDone { - /// The unique id of the entry. - id: u64, - /// The hash of the entry. - hash: Hash, - }, - /// We are done setting the entry to the doc. - AllDone { - /// The key of the entry - key: Bytes, - }, - /// We got an error and need to abort. - /// - /// This will be the last message in the stream. - Abort(serde_error::Error), -} - -/// Intended capability for document share tickets -#[derive(Serialize, Deserialize, Debug, Clone, Display, FromStr)] -pub enum ShareMode { - /// Read-only access - Read, - /// Write access - Write, -} -/// Progress stream for [`Doc::import_file`]. -#[derive(derive_more::Debug)] -#[must_use = "streams do nothing unless polled"] -pub struct ImportFileProgress { - #[debug(skip)] - stream: Pin> + Send + Unpin + 'static>>, -} - -impl ImportFileProgress { - fn new( - stream: (impl Stream, impl Into>> - + Send - + Unpin - + 'static), - ) -> Self { - let stream = stream.map(|item| match item { - Ok(item) => Ok(item.into()), - Err(err) => Err(err.into()), - }); - Self { - stream: Box::pin(stream), - } - } - - /// Finishes writing the stream, ignoring all intermediate progress events. - /// - /// Returns a [`ImportFileOutcome`] which contains a tag, key, and hash and the size of the - /// content. - pub async fn finish(mut self) -> Result { - let mut entry_size = 0; - let mut entry_hash = None; - while let Some(msg) = self.next().await { - match msg? { - ImportProgress::Found { size, .. } => { - entry_size = size; - } - ImportProgress::AllDone { key } => { - let hash = entry_hash - .context("expected DocImportProgress::IngestDone event to occur")?; - let outcome = ImportFileOutcome { - hash, - key, - size: entry_size, - }; - return Ok(outcome); - } - ImportProgress::Abort(err) => return Err(err.into()), - ImportProgress::Progress { .. } => {} - ImportProgress::IngestDone { hash, .. } => { - entry_hash = Some(hash); - } - } - } - Err(anyhow!("Response stream ended prematurely")) - } -} - -/// Outcome of a [`Doc::import_file`] operation -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ImportFileOutcome { - /// The hash of the entry's content - pub hash: Hash, - /// The size of the entry - pub size: u64, - /// The key of the entry - pub key: Bytes, -} - -impl Stream for ImportFileProgress { - type Item = Result; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.stream).poll_next(cx) - } -} - -/// Progress stream for [`Doc::export_file`]. -#[derive(derive_more::Debug)] -pub struct ExportFileProgress { - #[debug(skip)] - stream: Pin> + Send + Unpin + 'static>>, -} -impl ExportFileProgress { - fn new( - stream: (impl Stream, impl Into>> - + Send - + Unpin - + 'static), - ) -> Self { - let stream = stream.map(|item| match item { - Ok(item) => Ok(item.into()), - Err(err) => Err(err.into()), - }); - Self { - stream: Box::pin(stream), - } - } - - /// Iterates through the export progress stream, returning when the stream has completed. - /// - /// Returns a [`ExportFileOutcome`] which contains a file path the data was written to and the size of the content. - pub async fn finish(mut self) -> Result { - let mut total_size = 0; - let mut path = None; - while let Some(msg) = self.next().await { - match msg? { - ExportProgress::Found { size, outpath, .. } => { - total_size = size.value(); - path = Some(outpath); - } - ExportProgress::AllDone => { - let path = path.context("expected ExportProgress::Found event to occur")?; - let outcome = ExportFileOutcome { - size: total_size, - path, - }; - return Ok(outcome); - } - ExportProgress::Done { .. } => {} - ExportProgress::Abort(err) => return Err(anyhow!(err)), - ExportProgress::Progress { .. } => {} - } - } - Err(anyhow!("Response stream ended prematurely")) - } -} - -/// Outcome of a [`Doc::export_file`] operation -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExportFileOutcome { - /// The size of the entry - pub size: u64, - /// The path to which the entry was saved - pub path: PathBuf, -} - -impl Stream for ExportFileProgress { - type Item = Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.stream).poll_next(cx) - } -} diff --git a/src/rpc/docs_handle_request.rs b/src/rpc/docs_handle_request.rs deleted file mode 100644 index c7a1e73..0000000 --- a/src/rpc/docs_handle_request.rs +++ /dev/null @@ -1,549 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use anyhow::anyhow; -use futures_lite::{Stream, StreamExt}; -use iroh_blobs::{ - export::ExportProgress, - store::{ExportFormat, ImportProgress}, - util::progress::{AsyncChannelProgressSender, ProgressSender}, - BlobFormat, HashAndFormat, -}; - -use super::{ - client::docs::ShareMode, - proto::{ - AuthorCreateRequest, AuthorCreateResponse, AuthorDeleteRequest, AuthorDeleteResponse, - AuthorExportRequest, AuthorExportResponse, AuthorGetDefaultRequest, - AuthorGetDefaultResponse, AuthorImportRequest, AuthorImportResponse, AuthorListRequest, - AuthorListResponse, AuthorSetDefaultRequest, AuthorSetDefaultResponse, CloseRequest, - CloseResponse, CreateRequest as DocCreateRequest, CreateResponse as DocCreateResponse, - DelRequest, DelResponse, DocListRequest, DocSubscribeRequest, DocSubscribeResponse, - DropRequest, DropResponse, ExportFileRequest, ExportFileResponse, GetDownloadPolicyRequest, - GetDownloadPolicyResponse, GetExactRequest, GetExactResponse, GetManyRequest, - GetManyResponse, GetSyncPeersRequest, GetSyncPeersResponse, ImportFileRequest, - ImportFileResponse, ImportRequest as DocImportRequest, ImportResponse as DocImportResponse, - LeaveRequest, LeaveResponse, ListResponse as DocListResponse, OpenRequest, OpenResponse, - SetDownloadPolicyRequest, SetDownloadPolicyResponse, SetHashRequest, SetHashResponse, - SetRequest, SetResponse, ShareRequest, ShareResponse, StartSyncRequest, StartSyncResponse, - StatusRequest, StatusResponse, - }, - Handler, RpcError, RpcResult, -}; -use crate::{Author, DocTicket, NamespaceSecret}; - -/// Capacity for the flume channels to forward sync store iterators to async RPC streams. -const ITER_CHANNEL_CAP: usize = 64; - -impl Handler { - pub(super) async fn author_create( - self, - _req: AuthorCreateRequest, - ) -> RpcResult { - // TODO: pass rng - let author = Author::new(&mut rand::rngs::OsRng {}); - self.sync - .import_author(author.clone()) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(AuthorCreateResponse { - author_id: author.id(), - }) - } - - pub(super) async fn author_default( - self, - _req: AuthorGetDefaultRequest, - ) -> RpcResult { - let author_id = self.default_author.get(); - Ok(AuthorGetDefaultResponse { author_id }) - } - - pub(super) async fn author_set_default( - self, - req: AuthorSetDefaultRequest, - ) -> RpcResult { - self.default_author - .set(req.author_id, &self.sync) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(AuthorSetDefaultResponse) - } - - pub(super) fn author_list( - self, - _req: AuthorListRequest, - ) -> impl Stream> + Unpin { - let (tx, rx) = async_channel::bounded(ITER_CHANNEL_CAP); - let sync = self.sync.clone(); - // we need to spawn a task to send our request to the sync handle, because the method - // itself must be sync. - tokio::task::spawn(async move { - let tx2 = tx.clone(); - if let Err(err) = sync.list_authors(tx).await { - tx2.send(Err(err)).await.ok(); - } - }); - rx.boxed().map(|r| { - r.map(|author_id| AuthorListResponse { author_id }) - .map_err(|e| RpcError::new(&*e)) - }) - } - - pub(super) async fn author_import( - self, - req: AuthorImportRequest, - ) -> RpcResult { - let author_id = self - .sync - .import_author(req.author) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(AuthorImportResponse { author_id }) - } - - pub(super) async fn author_export( - self, - req: AuthorExportRequest, - ) -> RpcResult { - let author = self - .sync - .export_author(req.author) - .await - .map_err(|e| RpcError::new(&*e))?; - - Ok(AuthorExportResponse { author }) - } - - pub(super) async fn author_delete( - self, - req: AuthorDeleteRequest, - ) -> RpcResult { - if req.author == self.default_author.get() { - return Err(RpcError::new(&*anyhow!( - "Deleting the default author is not supported" - ))); - } - self.sync - .delete_author(req.author) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(AuthorDeleteResponse) - } - - pub(super) async fn doc_create(self, _req: DocCreateRequest) -> RpcResult { - let namespace = NamespaceSecret::new(&mut rand::rngs::OsRng {}); - let id = namespace.id(); - self.sync - .import_namespace(namespace.into()) - .await - .map_err(|e| RpcError::new(&*e))?; - self.sync - .open(id, Default::default()) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(DocCreateResponse { id }) - } - - pub(super) async fn doc_drop(self, req: DropRequest) -> RpcResult { - let DropRequest { doc_id } = req; - self.leave(doc_id, true) - .await - .map_err(|e| RpcError::new(&*e))?; - self.sync - .drop_replica(doc_id) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(DropResponse {}) - } - - pub(super) fn doc_list( - self, - _req: DocListRequest, - ) -> impl Stream> + Unpin { - let (tx, rx) = async_channel::bounded(ITER_CHANNEL_CAP); - let sync = self.sync.clone(); - // we need to spawn a task to send our request to the sync handle, because the method - // itself must be sync. - tokio::task::spawn(async move { - let tx2 = tx.clone(); - if let Err(err) = sync.list_replicas(tx).await { - tx2.send(Err(err)).await.ok(); - } - }); - rx.boxed().map(|r| { - r.map(|(id, capability)| DocListResponse { id, capability }) - .map_err(|e| RpcError::new(&*e)) - }) - } - - pub(super) async fn doc_open(self, req: OpenRequest) -> RpcResult { - self.sync - .open(req.doc_id, Default::default()) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(OpenResponse {}) - } - - pub(super) async fn doc_close(self, req: CloseRequest) -> RpcResult { - self.sync - .close(req.doc_id) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(CloseResponse {}) - } - - pub(super) async fn doc_status(self, req: StatusRequest) -> RpcResult { - let status = self - .sync - .get_state(req.doc_id) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(StatusResponse { status }) - } - - pub(super) async fn doc_share(self, req: ShareRequest) -> RpcResult { - let ShareRequest { - doc_id, - mode, - addr_options, - } = req; - let me = self - .endpoint - .node_addr() - .await - .map_err(|e| RpcError::new(&*e))?; - let me = addr_options.apply(&me); - - let capability = match mode { - ShareMode::Read => crate::Capability::Read(doc_id), - ShareMode::Write => { - let secret = self - .sync - .export_secret_key(doc_id) - .await - .map_err(|e| RpcError::new(&*e))?; - crate::Capability::Write(secret) - } - }; - self.start_sync(doc_id, vec![]) - .await - .map_err(|e| RpcError::new(&*e))?; - - Ok(ShareResponse(DocTicket { - capability, - nodes: vec![me], - })) - } - - pub(super) async fn doc_subscribe( - self, - req: DocSubscribeRequest, - ) -> RpcResult>> { - let stream = self - .subscribe(req.doc_id) - .await - .map_err(|e| RpcError::new(&*e))?; - - Ok(stream.map(|el| { - el.map(|event| DocSubscribeResponse { event }) - .map_err(|e| RpcError::new(&*e)) - })) - } - - pub(super) async fn doc_import(self, req: DocImportRequest) -> RpcResult { - let DocImportRequest { capability } = req; - let doc_id = self - .sync - .import_namespace(capability) - .await - .map_err(|e| RpcError::new(&*e))?; - self.sync - .open(doc_id, Default::default()) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(DocImportResponse { doc_id }) - } - - pub(super) async fn doc_start_sync( - self, - req: StartSyncRequest, - ) -> RpcResult { - let StartSyncRequest { doc_id, peers } = req; - self.start_sync(doc_id, peers) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(StartSyncResponse {}) - } - - pub(super) async fn doc_leave(self, req: LeaveRequest) -> RpcResult { - let LeaveRequest { doc_id } = req; - self.leave(doc_id, false) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(LeaveResponse {}) - } - - pub(super) async fn doc_set(self, req: SetRequest) -> RpcResult { - let blobs_store = self.blob_store(); - let SetRequest { - doc_id, - author_id, - key, - value, - } = req; - let len = value.len(); - let tag = blobs_store - .import_bytes(value, BlobFormat::Raw) - .await - .map_err(|e| RpcError::new(&e))?; - self.sync - .insert_local(doc_id, author_id, key.clone(), *tag.hash(), len as u64) - .await - .map_err(|e| RpcError::new(&*e))?; - let entry = self - .sync - .get_exact(doc_id, author_id, key, false) - .await - .map_err(|e| RpcError::new(&*e))? - .ok_or_else(|| RpcError::new(&*anyhow!("failed to get entry after insertion")))?; - Ok(SetResponse { entry }) - } - - pub(super) async fn doc_del(self, req: DelRequest) -> RpcResult { - let DelRequest { - doc_id, - author_id, - prefix, - } = req; - let removed = self - .sync - .delete_prefix(doc_id, author_id, prefix) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(DelResponse { removed }) - } - - pub(super) async fn doc_set_hash(self, req: SetHashRequest) -> RpcResult { - let SetHashRequest { - doc_id, - author_id, - key, - hash, - size, - } = req; - self.sync - .insert_local(doc_id, author_id, key.clone(), hash, size) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(SetHashResponse {}) - } - - pub(super) fn doc_get_many( - self, - req: GetManyRequest, - ) -> impl Stream> + Unpin { - let GetManyRequest { doc_id, query } = req; - let (tx, rx) = async_channel::bounded(ITER_CHANNEL_CAP); - let sync = self.sync.clone(); - // we need to spawn a task to send our request to the sync handle, because the method - // itself must be sync. - tokio::task::spawn(async move { - let tx2 = tx.clone(); - if let Err(err) = sync.get_many(doc_id, query, tx).await { - tx2.send(Err(err)).await.ok(); - } - }); - rx.boxed().map(|r| { - r.map(|entry| GetManyResponse { entry }) - .map_err(|e| RpcError::new(&*e)) - }) - } - - pub(super) async fn doc_get_exact(self, req: GetExactRequest) -> RpcResult { - let GetExactRequest { - doc_id, - author, - key, - include_empty, - } = req; - let entry = self - .sync - .get_exact(doc_id, author, key, include_empty) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(GetExactResponse { entry }) - } - - pub(super) async fn doc_set_download_policy( - self, - req: SetDownloadPolicyRequest, - ) -> RpcResult { - self.sync - .set_download_policy(req.doc_id, req.policy) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(SetDownloadPolicyResponse {}) - } - - pub(super) async fn doc_get_download_policy( - self, - req: GetDownloadPolicyRequest, - ) -> RpcResult { - let policy = self - .sync - .get_download_policy(req.doc_id) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(GetDownloadPolicyResponse { policy }) - } - - pub(super) async fn doc_get_sync_peers( - self, - req: GetSyncPeersRequest, - ) -> RpcResult { - let peers = self - .sync - .get_sync_peers(req.doc_id) - .await - .map_err(|e| RpcError::new(&*e))?; - Ok(GetSyncPeersResponse { peers }) - } - - pub(super) fn doc_import_file( - self, - msg: ImportFileRequest, - ) -> impl Stream { - // provide a little buffer so that we don't slow down the sender - let (tx, rx) = async_channel::bounded(32); - let tx2 = tx.clone(); - let this = self.clone(); - self.local_pool_handle().spawn_detached(|| async move { - if let Err(e) = this.doc_import_file0(msg, tx).await { - tx2.send(super::client::docs::ImportProgress::Abort(RpcError::new( - &*e, - ))) - .await - .ok(); - } - }); - rx.map(ImportFileResponse) - } - - async fn doc_import_file0( - self, - msg: ImportFileRequest, - progress: async_channel::Sender, - ) -> anyhow::Result<()> { - use std::collections::BTreeMap; - - use iroh_blobs::store::ImportMode; - - use super::client::docs::ImportProgress as DocImportProgress; - - let progress = AsyncChannelProgressSender::new(progress); - let names = Arc::new(Mutex::new(BTreeMap::new())); - // convert import progress to provide progress - let import_progress = progress.clone().with_filter_map(move |x| match x { - ImportProgress::Found { id, name } => { - names.lock().unwrap().insert(id, name); - None - } - ImportProgress::Size { id, size } => { - let name = names.lock().unwrap().remove(&id)?; - Some(DocImportProgress::Found { id, name, size }) - } - ImportProgress::OutboardProgress { id, offset } => { - Some(DocImportProgress::Progress { id, offset }) - } - ImportProgress::OutboardDone { hash, id } => { - Some(DocImportProgress::IngestDone { hash, id }) - } - _ => None, - }); - let ImportFileRequest { - doc_id, - author_id, - key, - path: root, - in_place, - } = msg; - // Check that the path is absolute and exists. - anyhow::ensure!(root.is_absolute(), "path must be absolute"); - anyhow::ensure!( - root.exists(), - "trying to add missing path: {}", - root.display() - ); - - let import_mode = match in_place { - true => ImportMode::TryReference, - false => ImportMode::Copy, - }; - - let blobs = self.blob_store(); - let (temp_tag, size) = blobs - .import_file(root, import_mode, BlobFormat::Raw, import_progress) - .await?; - - let hash_and_format = temp_tag.inner(); - let HashAndFormat { hash, .. } = *hash_and_format; - self.doc_set_hash(SetHashRequest { - doc_id, - author_id, - key: key.clone(), - hash, - size, - }) - .await?; - drop(temp_tag); - progress.send(DocImportProgress::AllDone { key }).await?; - Ok(()) - } - - pub(super) fn doc_export_file( - self, - msg: ExportFileRequest, - ) -> impl Stream { - let (tx, rx) = async_channel::bounded(1024); - let tx2 = tx.clone(); - let this = self.clone(); - self.local_pool_handle().spawn_detached(|| async move { - if let Err(e) = this.doc_export_file0(msg, tx).await { - tx2.send(ExportProgress::Abort(RpcError::new(&*e))) - .await - .ok(); - } - }); - rx.map(ExportFileResponse) - } - - async fn doc_export_file0( - self, - msg: ExportFileRequest, - progress: async_channel::Sender, - ) -> anyhow::Result<()> { - let progress = AsyncChannelProgressSender::new(progress); - let ExportFileRequest { entry, path, mode } = msg; - let key = bytes::Bytes::from(entry.key().to_vec()); - let export_progress = progress.clone().with_map(move |mut x| { - // assign the doc key to the `meta` field of the initial progress event - if let ExportProgress::Found { meta, .. } = &mut x { - *meta = Some(key.clone()) - } - x - }); - - let blobs = self.blob_store(); - iroh_blobs::export::export( - blobs, - entry.content_hash(), - path, - ExportFormat::Blob, - mode, - export_progress, - ) - .await?; - progress.send(ExportProgress::AllDone).await?; - Ok(()) - } -} diff --git a/src/rpc/proto.rs b/src/rpc/proto.rs deleted file mode 100644 index 99876fb..0000000 --- a/src/rpc/proto.rs +++ /dev/null @@ -1,539 +0,0 @@ -//! Protocol definitions for RPC. - -use std::path::PathBuf; - -use bytes::Bytes; -use iroh::NodeAddr; -use iroh_blobs::{export::ExportProgress, store::ExportMode, Hash}; -use nested_enum_utils::enum_conversions; -use quic_rpc::pattern::try_server_streaming::StreamCreated; -use quic_rpc_derive::rpc_requests; -use serde::{Deserialize, Serialize}; - -use super::{ - client::docs::{ImportProgress, ShareMode}, - AddrInfoOptions, RpcError, RpcResult, -}; -use crate::{ - actor::OpenState, - engine::LiveEvent, - store::{DownloadPolicy, Query}, - Author, AuthorId, Capability, CapabilityKind, DocTicket, Entry, NamespaceId, PeerIdBytes, - SignedEntry, -}; - -/// The RPC service type for the docs protocol. -#[derive(Debug, Clone)] -pub struct RpcService; - -impl quic_rpc::Service for RpcService { - type Req = Request; - type Res = Response; -} - -#[allow(missing_docs)] -#[derive(strum::Display, Debug, Serialize, Deserialize)] -#[enum_conversions] -#[rpc_requests(RpcService)] -pub enum Request { - #[rpc(response = RpcResult)] - Open(OpenRequest), - #[rpc(response = RpcResult)] - Close(CloseRequest), - #[rpc(response = RpcResult)] - Status(StatusRequest), - #[server_streaming(response = RpcResult)] - List(DocListRequest), - #[rpc(response = RpcResult)] - Create(CreateRequest), - #[rpc(response = RpcResult)] - Drop(DropRequest), - #[rpc(response = RpcResult)] - Import(ImportRequest), - #[rpc(response = RpcResult)] - Set(SetRequest), - #[rpc(response = RpcResult)] - SetHash(SetHashRequest), - #[server_streaming(response = RpcResult)] - Get(GetManyRequest), - #[rpc(response = RpcResult)] - GetExact(GetExactRequest), - #[server_streaming(response = ImportFileResponse)] - ImportFile(ImportFileRequest), - #[server_streaming(response = ExportFileResponse)] - ExportFile(ExportFileRequest), - #[rpc(response = RpcResult)] - Del(DelRequest), - #[rpc(response = RpcResult)] - StartSync(StartSyncRequest), - #[rpc(response = RpcResult)] - Leave(LeaveRequest), - #[rpc(response = RpcResult)] - Share(ShareRequest), - #[try_server_streaming(create_error = RpcError, item_error = RpcError, item = DocSubscribeResponse)] - Subscribe(DocSubscribeRequest), - #[rpc(response = RpcResult)] - GetDownloadPolicy(GetDownloadPolicyRequest), - #[rpc(response = RpcResult)] - SetDownloadPolicy(SetDownloadPolicyRequest), - #[rpc(response = RpcResult)] - GetSyncPeers(GetSyncPeersRequest), - #[server_streaming(response = RpcResult)] - AuthorList(AuthorListRequest), - #[rpc(response = RpcResult)] - AuthorCreate(AuthorCreateRequest), - #[rpc(response = RpcResult)] - AuthorGetDefault(AuthorGetDefaultRequest), - #[rpc(response = RpcResult)] - AuthorSetDefault(AuthorSetDefaultRequest), - #[rpc(response = RpcResult)] - AuthorImport(AuthorImportRequest), - #[rpc(response = RpcResult)] - AuthorExport(AuthorExportRequest), - #[rpc(response = RpcResult)] - AuthorDelete(AuthorDeleteRequest), -} - -#[allow(missing_docs)] -#[derive(strum::Display, Debug, Serialize, Deserialize)] -#[enum_conversions] -pub enum Response { - Open(RpcResult), - Close(RpcResult), - Status(RpcResult), - List(RpcResult), - Create(RpcResult), - Drop(RpcResult), - Import(RpcResult), - Set(RpcResult), - SetHash(RpcResult), - Get(RpcResult), - GetExact(RpcResult), - ImportFile(ImportFileResponse), - ExportFile(ExportFileResponse), - Del(RpcResult), - Share(RpcResult), - StartSync(RpcResult), - Leave(RpcResult), - Subscribe(RpcResult), - GetDownloadPolicy(RpcResult), - SetDownloadPolicy(RpcResult), - GetSyncPeers(RpcResult), - StreamCreated(RpcResult), - AuthorList(RpcResult), - AuthorCreate(RpcResult), - AuthorGetDefault(RpcResult), - AuthorSetDefault(RpcResult), - AuthorImport(RpcResult), - AuthorExport(RpcResult), - AuthorDelete(RpcResult), -} - -/// Subscribe to events for a document. -#[derive(Serialize, Deserialize, Debug)] -pub struct DocSubscribeRequest { - /// The document id - pub doc_id: NamespaceId, -} - -/// Response to [`DocSubscribeRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct DocSubscribeResponse { - /// The event that occurred on the document - pub event: LiveEvent, -} - -/// List all documents -#[derive(Serialize, Deserialize, Debug)] -pub struct DocListRequest {} - -/// Response to [`DocListRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct ListResponse { - /// The document id - pub id: NamespaceId, - /// The capability over the document. - pub capability: CapabilityKind, -} - -/// Create a new document -#[derive(Serialize, Deserialize, Debug)] -pub struct CreateRequest {} - -/// Response to [`CreateRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct CreateResponse { - /// The document id - pub id: NamespaceId, -} - -/// Import a document from a capability. -#[derive(Serialize, Deserialize, Debug)] -pub struct ImportRequest { - /// The namespace capability. - pub capability: Capability, -} - -/// Response to [`ImportRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct ImportResponse { - /// the document id - pub doc_id: NamespaceId, -} - -/// Share a document with peers over a ticket. -#[derive(Serialize, Deserialize, Debug)] -pub struct ShareRequest { - /// The document id - pub doc_id: NamespaceId, - /// Whether to share read or write access to the document - pub mode: ShareMode, - /// Configuration of the addresses in the ticket. - pub addr_options: AddrInfoOptions, -} - -/// The response to [`ShareRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct ShareResponse(pub DocTicket); - -/// Get info on a document -#[derive(Serialize, Deserialize, Debug)] -pub struct StatusRequest { - /// The document id - pub doc_id: NamespaceId, -} - -/// Response to [`StatusRequest`] -// TODO: actually provide info -#[derive(Serialize, Deserialize, Debug)] -pub struct StatusResponse { - /// Live sync status - pub status: OpenState, -} - -/// Open a document -#[derive(Serialize, Deserialize, Debug)] -pub struct OpenRequest { - /// The document id - pub doc_id: NamespaceId, -} - -/// Response to [`OpenRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct OpenResponse {} - -/// Open a document -#[derive(Serialize, Deserialize, Debug)] -pub struct CloseRequest { - /// The document id - pub doc_id: NamespaceId, -} - -/// Response to [`CloseRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct CloseResponse {} - -/// Start to sync a doc with peers. -#[derive(Serialize, Deserialize, Debug)] -pub struct StartSyncRequest { - /// The document id - pub doc_id: NamespaceId, - /// List of peers to join - pub peers: Vec, -} - -/// Response to [`StartSyncRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct StartSyncResponse {} - -/// Stop the live sync for a doc, and optionally delete the document. -#[derive(Serialize, Deserialize, Debug)] -pub struct LeaveRequest { - /// The document id - pub doc_id: NamespaceId, -} - -/// Response to [`LeaveRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct LeaveResponse {} - -/// Stop the live sync for a doc, and optionally delete the document. -#[derive(Serialize, Deserialize, Debug)] -pub struct DropRequest { - /// The document id - pub doc_id: NamespaceId, -} - -/// Response to [`DropRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct DropResponse {} - -/// Set an entry in a document -#[derive(Serialize, Deserialize, Debug)] -pub struct SetRequest { - /// The document id - pub doc_id: NamespaceId, - /// Author of this entry. - pub author_id: AuthorId, - /// Key of this entry. - pub key: Bytes, - /// Value of this entry. - // TODO: Allow to provide the hash directly - // TODO: Add a way to provide content as stream - pub value: Bytes, -} - -/// Response to [`SetRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct SetResponse { - /// The newly-created entry. - pub entry: SignedEntry, -} - -/// A request to the node to add the data at the given filepath as an entry to the document -/// -/// Will produce a stream of [`ImportProgress`] messages. -#[derive(Debug, Serialize, Deserialize)] -pub struct ImportFileRequest { - /// The document id - pub doc_id: NamespaceId, - /// Author of this entry. - pub author_id: AuthorId, - /// Key of this entry. - pub key: Bytes, - /// The filepath to the data - /// - /// This should be an absolute path valid for the file system on which - /// the node runs. Usually the cli will run on the same machine as the - /// node, so this should be an absolute path on the cli machine. - pub path: PathBuf, - /// True if the provider can assume that the data will not change, so it - /// can be shared in place. - pub in_place: bool, -} - -/// Wrapper around [`ImportProgress`]. -#[derive(Debug, Serialize, Deserialize, derive_more::Into)] -pub struct ImportFileResponse(pub ImportProgress); - -/// A request to the node to save the data of the entry to the given filepath -/// -/// Will produce a stream of [`ExportFileResponse`] messages. -#[derive(Debug, Serialize, Deserialize)] -pub struct ExportFileRequest { - /// The entry you want to export - pub entry: Entry, - /// The filepath to where the data should be saved - /// - /// This should be an absolute path valid for the file system on which - /// the node runs. Usually the cli will run on the same machine as the - /// node, so this should be an absolute path on the cli machine. - pub path: PathBuf, - /// The mode of exporting. Setting to `ExportMode::TryReference` means attempting - /// to use references for keeping file - pub mode: ExportMode, -} - -/// Progress messages for an doc export operation -/// -/// An export operation involves reading the entry from the database ans saving the entry to the -/// given `outpath` -#[derive(Debug, Serialize, Deserialize, derive_more::Into)] -pub struct ExportFileResponse(pub ExportProgress); - -/// Delete entries in a document -#[derive(Serialize, Deserialize, Debug)] -pub struct DelRequest { - /// The document id. - pub doc_id: NamespaceId, - /// Author of this entry. - pub author_id: AuthorId, - /// Prefix to delete. - pub prefix: Bytes, -} - -/// Response to [`DelRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct DelResponse { - /// The number of entries that were removed. - pub removed: usize, -} - -/// Set an entry in a document via its hash -#[derive(Serialize, Deserialize, Debug)] -pub struct SetHashRequest { - /// The document id - pub doc_id: NamespaceId, - /// Author of this entry. - pub author_id: AuthorId, - /// Key of this entry. - pub key: Bytes, - /// Hash of this entry. - pub hash: Hash, - /// Size of this entry. - pub size: u64, -} - -/// Response to [`SetHashRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct SetHashResponse {} - -/// Get entries from a document -#[derive(Serialize, Deserialize, Debug)] -pub struct GetManyRequest { - /// The document id - pub doc_id: NamespaceId, - /// Query to run - pub query: Query, -} - -/// Response to [`GetManyRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct GetManyResponse { - /// The document entry - pub entry: SignedEntry, -} - -/// Get entries from a document -#[derive(Serialize, Deserialize, Debug)] -pub struct GetExactRequest { - /// The document id - pub doc_id: NamespaceId, - /// Key matcher - pub key: Bytes, - /// Author matcher - pub author: AuthorId, - /// Whether to include empty entries (prefix deletion markers) - pub include_empty: bool, -} - -/// Response to [`GetExactRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct GetExactResponse { - /// The document entry - pub entry: Option, -} - -/// Set a download policy -#[derive(Serialize, Deserialize, Debug)] -pub struct SetDownloadPolicyRequest { - /// The document id - pub doc_id: NamespaceId, - /// Download policy - pub policy: DownloadPolicy, -} - -/// Response to [`SetDownloadPolicyRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct SetDownloadPolicyResponse {} - -/// Get a download policy -#[derive(Serialize, Deserialize, Debug)] -pub struct GetDownloadPolicyRequest { - /// The document id - pub doc_id: NamespaceId, -} - -/// Response to [`GetDownloadPolicyRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct GetDownloadPolicyResponse { - /// The download policy - pub policy: DownloadPolicy, -} - -/// Get peers for document -#[derive(Serialize, Deserialize, Debug)] -pub struct GetSyncPeersRequest { - /// The document id - pub doc_id: NamespaceId, -} - -/// Response to [`GetSyncPeersRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct GetSyncPeersResponse { - /// List of peers ids - pub peers: Option>, -} - -/// List document authors for which we have a secret key. -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorListRequest {} - -/// Response for [`AuthorListRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorListResponse { - /// The author id - pub author_id: AuthorId, -} - -/// Create a new document author. -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorCreateRequest; - -/// Response for [`AuthorCreateRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorCreateResponse { - /// The id of the created author - pub author_id: AuthorId, -} - -/// Get the default author. -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorGetDefaultRequest; - -/// Response for [`AuthorGetDefaultRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorGetDefaultResponse { - /// The id of the author - pub author_id: AuthorId, -} - -/// Set the default author. -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorSetDefaultRequest { - /// The id of the author - pub author_id: AuthorId, -} - -/// Response for [`AuthorSetDefaultRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorSetDefaultResponse; - -/// Delete an author -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorDeleteRequest { - /// The id of the author to delete - pub author: AuthorId, -} - -/// Response for [`AuthorDeleteRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorDeleteResponse; - -/// Exports an author -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorExportRequest { - /// The id of the author to delete - pub author: AuthorId, -} - -/// Response for [`AuthorExportRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorExportResponse { - /// The author - pub author: Option, -} - -/// Import author from secret key -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorImportRequest { - /// The author to import - pub author: Author, -} - -/// Response to [`ImportRequest`] -#[derive(Serialize, Deserialize, Debug)] -pub struct AuthorImportResponse { - /// The author id of the imported author - pub author_id: AuthorId, -} diff --git a/src/store/fs.rs b/src/store/fs.rs index 5ec6e07..78079e7 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -77,6 +77,7 @@ impl AsMut for Store { } #[derive(derive_more::Debug, Default)] +#[allow(clippy::large_enum_variant)] enum CurrentTransaction { #[default] None, diff --git a/src/store/fs/query.rs b/src/store/fs/query.rs index e04f986..9393afd 100644 --- a/src/store/fs/query.rs +++ b/src/store/fs/query.rs @@ -25,6 +25,7 @@ pub struct QueryIterator { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] enum QueryRange { AuthorKey { range: RecordsRange<'static>, diff --git a/src/store/util.rs b/src/store/util.rs index 04cad47..07e7bdc 100644 --- a/src/store/util.rs +++ b/src/store/util.rs @@ -46,6 +46,7 @@ impl From<&Query> for IndexKind { #[derive(Debug, Default)] pub struct LatestPerKeySelector(Option); +#[allow(clippy::large_enum_variant)] pub enum SelectorRes { /// The iterator is finished. Finished, diff --git a/src/sync.rs b/src/sync.rs index 382083a..8728f05 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -40,7 +40,8 @@ pub type PeerIdBytes = [u8; 32]; pub const MAX_TIMESTAMP_FUTURE_SHIFT: u64 = 10 * 60 * Duration::from_secs(1).as_millis() as u64; /// Callback that may be set on a replica to determine the availability status for a content hash. -pub type ContentStatusCallback = Arc ContentStatus + Send + Sync + 'static>; +pub type ContentStatusCallback = + Arc n0_future::boxed::BoxFuture + Send + Sync + 'static>; /// Event emitted by sync when entries are added. #[derive(Debug, Clone)] @@ -497,7 +498,7 @@ where /// Process a set reconciliation message from a remote peer. /// /// Returns the next message to be sent to the peer, if any. - pub fn sync_process_message( + pub async fn sync_process_message( &mut self, message: crate::ranger::Message, from_peer: PeerIdBytes, @@ -522,40 +523,46 @@ where .store .get_download_policy(&my_namespace) .unwrap_or_default(); - let reply = self.store.process_message( - &Default::default(), - message, - // validate callback: validate incoming entries, and send to on_insert channel - |store, entry, content_status| { - let origin = InsertOrigin::Sync { - from: from_peer, - remote_content_status: content_status, - }; - validate_entry(now, store, my_namespace, entry, &origin).is_ok() - }, - // on_insert callback: is called when an entry was actually inserted in the store - |_store, entry, content_status| { - // We use `send_with` to only clone the entry if we have active subscriptions. - self.info.subscribers.send_with(|| { - let should_download = download_policy.matches(entry.entry()); - Event::RemoteInsert { + let reply = self + .store + .process_message( + &Default::default(), + message, + // validate callback: validate incoming entries, and send to on_insert channel + |store, entry, content_status| { + let origin = InsertOrigin::Sync { from: from_peer, - namespace: my_namespace, - entry: entry.clone(), - should_download, remote_content_status: content_status, - } - }) - }, - // content_status callback: get content status for outgoing entries - |_store, entry| { - if let Some(cb) = cb.as_ref() { - cb(entry.content_hash()) - } else { - ContentStatus::Missing - } - }, - )?; + }; + validate_entry(now, store, my_namespace, entry, &origin).is_ok() + }, + // on_insert callback: is called when an entry was actually inserted in the store + |_store, entry, content_status| { + // We use `send_with` to only clone the entry if we have active subscriptions. + self.info.subscribers.send_with(|| { + let should_download = download_policy.matches(entry.entry()); + Event::RemoteInsert { + from: from_peer, + namespace: my_namespace, + entry: entry.clone(), + should_download, + remote_content_status: content_status, + } + }) + }, + // content_status callback: get content status for outgoing entries + move |entry| { + let cb = cb.clone(); + Box::pin(async move { + if let Some(cb) = cb.as_ref() { + cb(entry.content_hash()).await + } else { + ContentStatus::Missing + } + }) + }, + ) + .await?; // update state with outgoing data. if let Some(ref reply) = reply { @@ -1571,27 +1578,27 @@ mod tests { Ok(()) } - #[test] - fn test_replica_sync_memory() -> Result<()> { + #[tokio::test] + async fn test_replica_sync_memory() -> Result<()> { let alice_store = store::Store::memory(); let bob_store = store::Store::memory(); - test_replica_sync(alice_store, bob_store)?; + test_replica_sync(alice_store, bob_store).await?; Ok(()) } - #[test] - fn test_replica_sync_fs() -> Result<()> { + #[tokio::test] + async fn test_replica_sync_fs() -> Result<()> { let alice_dbfile = tempfile::NamedTempFile::new()?; let alice_store = store::fs::Store::persistent(alice_dbfile.path())?; let bob_dbfile = tempfile::NamedTempFile::new()?; let bob_store = store::fs::Store::persistent(bob_dbfile.path())?; - test_replica_sync(alice_store, bob_store)?; + test_replica_sync(alice_store, bob_store).await?; Ok(()) } - fn test_replica_sync(mut alice_store: Store, mut bob_store: Store) -> Result<()> { + async fn test_replica_sync(mut alice_store: Store, mut bob_store: Store) -> Result<()> { let alice_set = ["ape", "eel", "fox", "gnu"]; let bob_set = ["bee", "cat", "doe", "eel", "fox", "hog"]; @@ -1608,7 +1615,7 @@ mod tests { bob.hash_and_insert(el, &author, el.as_bytes())?; } - let (alice_out, bob_out) = sync(&mut alice, &mut bob)?; + let (alice_out, bob_out) = sync(&mut alice, &mut bob).await?; assert_eq!(alice_out.num_sent, 2); assert_eq!(bob_out.num_recv, 2); @@ -1624,27 +1631,30 @@ mod tests { Ok(()) } - #[test] - fn test_replica_timestamp_sync_memory() -> Result<()> { + #[tokio::test] + async fn test_replica_timestamp_sync_memory() -> Result<()> { let alice_store = store::Store::memory(); let bob_store = store::Store::memory(); - test_replica_timestamp_sync(alice_store, bob_store)?; + test_replica_timestamp_sync(alice_store, bob_store).await?; Ok(()) } - #[test] - fn test_replica_timestamp_sync_fs() -> Result<()> { + #[tokio::test] + async fn test_replica_timestamp_sync_fs() -> Result<()> { let alice_dbfile = tempfile::NamedTempFile::new()?; let alice_store = store::fs::Store::persistent(alice_dbfile.path())?; let bob_dbfile = tempfile::NamedTempFile::new()?; let bob_store = store::fs::Store::persistent(bob_dbfile.path())?; - test_replica_timestamp_sync(alice_store, bob_store)?; + test_replica_timestamp_sync(alice_store, bob_store).await?; Ok(()) } - fn test_replica_timestamp_sync(mut alice_store: Store, mut bob_store: Store) -> Result<()> { + async fn test_replica_timestamp_sync( + mut alice_store: Store, + mut bob_store: Store, + ) -> Result<()> { let mut rng = rand::thread_rng(); let author = Author::new(&mut rng); let namespace = NamespaceSecret::new(&mut rng); @@ -1657,7 +1667,7 @@ mod tests { let _alice_hash = alice.hash_and_insert(key, &author, alice_value)?; // system time increased - sync should overwrite let bob_hash = bob.hash_and_insert(key, &author, bob_value)?; - sync(&mut alice, &mut bob)?; + sync(&mut alice, &mut bob).await?; assert_eq!( get_content_hash(&mut alice_store, namespace.id(), author.id(), key)?, Some(bob_hash) @@ -1674,7 +1684,7 @@ mod tests { // system time increased - sync should overwrite let _bob_hash_2 = bob.hash_and_insert(key, &author, bob_value)?; let alice_hash_2 = alice.hash_and_insert(key, &author, alice_value_2)?; - sync(&mut alice, &mut bob)?; + sync(&mut alice, &mut bob).await?; assert_eq!( get_content_hash(&mut alice_store, namespace.id(), author.id(), key)?, Some(alice_hash_2) @@ -1813,24 +1823,24 @@ mod tests { Ok(()) } - #[test] - fn test_replica_sync_delete_memory() -> Result<()> { + #[tokio::test] + async fn test_replica_sync_delete_memory() -> Result<()> { let alice_store = store::Store::memory(); let bob_store = store::Store::memory(); - test_replica_sync_delete(alice_store, bob_store) + test_replica_sync_delete(alice_store, bob_store).await } - #[test] - fn test_replica_sync_delete_fs() -> Result<()> { + #[tokio::test] + async fn test_replica_sync_delete_fs() -> Result<()> { let alice_dbfile = tempfile::NamedTempFile::new()?; let alice_store = store::fs::Store::persistent(alice_dbfile.path())?; let bob_dbfile = tempfile::NamedTempFile::new()?; let bob_store = store::fs::Store::persistent(bob_dbfile.path())?; - test_replica_sync_delete(alice_store, bob_store) + test_replica_sync_delete(alice_store, bob_store).await } - fn test_replica_sync_delete(mut alice_store: Store, mut bob_store: Store) -> Result<()> { + async fn test_replica_sync_delete(mut alice_store: Store, mut bob_store: Store) -> Result<()> { let alice_set = ["foot"]; let bob_set = ["fool", "foo", "fog"]; @@ -1847,7 +1857,7 @@ mod tests { bob.hash_and_insert(el, &author, el.as_bytes())?; } - sync(&mut alice, &mut bob)?; + sync(&mut alice, &mut bob).await?; check_entries(&mut alice_store, &myspace.id(), &author, &alice_set)?; check_entries(&mut alice_store, &myspace.id(), &author, &bob_set)?; @@ -1858,7 +1868,7 @@ mod tests { let mut bob = bob_store.new_replica(myspace.clone())?; alice.delete_prefix("foo", &author)?; bob.hash_and_insert("fooz", &author, "fooz".as_bytes())?; - sync(&mut alice, &mut bob)?; + sync(&mut alice, &mut bob).await?; check_entries(&mut alice_store, &myspace.id(), &author, &["fog", "fooz"])?; check_entries(&mut bob_store, &myspace.id(), &author, &["fog", "fooz"])?; alice_store.flush()?; @@ -2186,8 +2196,8 @@ mod tests { /// This tests that no events are emitted for entries received during sync which are obsolete /// (too old) by the time they are actually inserted in the store. - #[test] - fn test_replica_no_wrong_remote_insert_events() -> Result<()> { + #[tokio::test] + async fn test_replica_no_wrong_remote_insert_events() -> Result<()> { let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(1); let mut store1 = store::Store::memory(); let mut store2 = store::Store::memory(); @@ -2212,10 +2222,12 @@ mod tests { let from1 = replica1.sync_initial_message()?; let from2 = replica2 .sync_process_message(from1, peer1, &mut state2) + .await .unwrap() .unwrap(); let from1 = replica1 .sync_process_message(from2, peer2, &mut state1) + .await .unwrap() .unwrap(); // now we will receive the entry from rpelica1. we will insert a newer entry now, while the @@ -2224,6 +2236,7 @@ mod tests { replica2.hash_and_insert(b"foo", &author, b"update")?; let from2 = replica2 .sync_process_message(from1, peer1, &mut state2) + .await .unwrap(); assert!(from2.is_none()); let events1 = drain(events1); @@ -2505,7 +2518,10 @@ mod tests { Ok(hash) } - fn sync(alice: &mut Replica, bob: &mut Replica) -> Result<(SyncOutcome, SyncOutcome)> { + async fn sync<'a>( + alice: &'a mut Replica<'a>, + bob: &'a mut Replica<'a>, + ) -> Result<(SyncOutcome, SyncOutcome)> { let alice_peer_id = [1u8; 32]; let bob_peer_id = [2u8; 32]; let mut alice_state = SyncOutcome::default(); @@ -2516,9 +2532,14 @@ mod tests { while let Some(msg) = next_to_bob.take() { assert!(rounds < 100, "too many rounds"); rounds += 1; - println!("round {}", rounds); - if let Some(msg) = bob.sync_process_message(msg, alice_peer_id, &mut bob_state)? { - next_to_bob = alice.sync_process_message(msg, bob_peer_id, &mut alice_state)? + println!("round {rounds}"); + if let Some(msg) = bob + .sync_process_message(msg, alice_peer_id, &mut bob_state) + .await? + { + next_to_bob = alice + .sync_process_message(msg, bob_peer_id, &mut alice_state) + .await? } } assert_eq!(alice_state.num_sent, bob_state.num_recv); diff --git a/src/ticket.rs b/src/ticket.rs index cb78098..8301ac4 100644 --- a/src/ticket.rs +++ b/src/ticket.rs @@ -34,11 +34,13 @@ impl ticket::Ticket for DocTicket { postcard::to_stdvec(&data).expect("postcard serialization failed") } - fn from_bytes(bytes: &[u8]) -> Result { - let res: TicketWireFormat = postcard::from_bytes(bytes).map_err(ticket::Error::Postcard)?; + fn from_bytes(bytes: &[u8]) -> Result { + let res: TicketWireFormat = postcard::from_bytes(bytes)?; let TicketWireFormat::Variant0(res) = res; if res.nodes.is_empty() { - return Err(ticket::Error::Verify("addressing info cannot be empty")); + return Err(ticket::ParseError::verification_failed( + "addressing info cannot be empty", + )); } Ok(res) } @@ -55,7 +57,7 @@ impl DocTicket { } impl std::str::FromStr for DocTicket { - type Err = ticket::Error; + type Err = ticket::ParseError; fn from_str(s: &str) -> Result { ticket::Ticket::deserialize(s) } diff --git a/tests/client.rs b/tests/client.rs index f932eeb..8175d9a 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -1,16 +1,16 @@ -#![cfg(feature = "rpc")] use anyhow::{Context, Result}; -use futures_util::TryStreamExt; -use iroh_blobs::{ - store::ExportMode, - util::fs::{key_to_path, path_to_key}, -}; +use iroh_blobs::api::blobs::{ExportMode, ImportMode}; use iroh_docs::store::Query; +use n0_future::StreamExt; use rand::RngCore; use testresult::TestResult; use tokio::io::AsyncWriteExt; use tracing_test::traced_test; -use util::Node; + +use self::util::{ + path::{key_to_path, path_to_key}, + Node, +}; mod util; @@ -19,7 +19,8 @@ mod util; #[traced_test] async fn test_doc_close() -> Result<()> { let node = Node::memory().spawn().await?; - let author = node.authors().default().await?; + // let author = node.authors().default().await?; + let author = node.docs().author_default().await?; // open doc two times let doc1 = node.docs().create().await?; let doc2 = node.docs().open(doc1.id()).await?.expect("doc to exist"); @@ -64,21 +65,27 @@ async fn test_doc_import_export() -> TestResult<()> { // create doc & author let client = node.client(); + let blobs = client.blobs(); let docs_client = client.docs(); let doc = docs_client.create().await.context("doc create")?; - let author = client.authors().create().await.context("author create")?; + // let author = client.authors().create().await.context("author create")?; + let author = client + .docs() + .author_create() + .await + .context("author create")?; // import file let import_outcome = doc .import_file( + blobs, author, path_to_key(path.clone(), None, Some(in_root))?, path, - true, + ImportMode::TryReference, ) .await .context("import file")? - .finish() .await .context("import finish")?; @@ -89,21 +96,25 @@ async fn test_doc_import_export() -> TestResult<()> { .context("get one")? .unwrap(); let key = entry.key().to_vec(); - let export_outcome = doc - .export_file( - entry, - key_to_path(key, None, Some(out_root))?, - ExportMode::Copy, - ) - .await - .context("export file")? - .finish() - .await - .context("export finish")?; - - let got_bytes = tokio::fs::read(export_outcome.path) - .await - .context("tokio read")?; + let path = key_to_path(key, None, Some(out_root))?; + // TODO(Frando): iroh-blobs should do this IMO. + // tokio::fs::create_dir_all(path.parent().unwrap()).await?; + let progress = doc + .export_file(blobs, entry, path.clone(), ExportMode::Copy) + .await?; + let mut progress = progress.stream().await; + while let Some(msg) = progress.next().await { + println!("MSG {msg:?}"); + } + // let _export_outcome = doc + // .export_file(blobs, entry, path.clone(), ExportMode::Copy) + // .await + // .context("export file")? + // .finish() + // .await + // .context("export finish")?; + + let got_bytes = tokio::fs::read(path).await.context("tokio read")?; assert_eq!(buf, got_bytes); Ok(()) @@ -114,33 +125,33 @@ async fn test_authors() -> Result<()> { let node = Node::memory().spawn().await?; // default author always exists - let authors: Vec<_> = node.authors().list().await?.try_collect().await?; + let authors: Vec<_> = node.docs().author_list().await?.try_collect().await?; assert_eq!(authors.len(), 1); - let default_author = node.authors().default().await?; + let default_author = node.docs().author_default().await?; assert_eq!(authors, vec![default_author]); - let author_id = node.authors().create().await?; + let author_id = node.docs().author_create().await?; - let authors: Vec<_> = node.authors().list().await?.try_collect().await?; + let authors: Vec<_> = node.docs().author_list().await?.try_collect().await?; assert_eq!(authors.len(), 2); let author = node - .authors() - .export(author_id) + .docs() + .author_export(author_id) .await? .expect("should have author"); - node.authors().delete(author_id).await?; - let authors: Vec<_> = node.authors().list().await?.try_collect().await?; + node.docs().author_delete(author_id).await?; + let authors: Vec<_> = node.docs().author_list().await?.try_collect().await?; assert_eq!(authors.len(), 1); - node.authors().import(author).await?; + node.docs().author_import(author).await?; - let authors: Vec<_> = node.authors().list().await?.try_collect().await?; + let authors: Vec<_> = node.docs().author_list().await?.try_collect().await?; assert_eq!(authors.len(), 2); - assert!(node.authors().default().await? != author_id); - node.authors().set_default(author_id).await?; - assert_eq!(node.authors().default().await?, author_id); + assert!(node.docs().author_default().await? != author_id); + node.docs().author_set_default(author_id).await?; + assert_eq!(node.docs().author_default().await?, author_id); Ok(()) } @@ -148,9 +159,9 @@ async fn test_authors() -> Result<()> { #[tokio::test] async fn test_default_author_memory() -> Result<()> { let iroh = Node::memory().spawn().await?; - let author = iroh.authors().default().await?; - assert!(iroh.authors().export(author).await?.is_some()); - assert!(iroh.authors().delete(author).await.is_err()); + let author = iroh.docs().author_default().await?; + assert!(iroh.docs().author_export(author).await?.is_some()); + assert!(iroh.docs().author_delete(author).await.is_err()); Ok(()) } @@ -163,9 +174,9 @@ async fn test_default_author_persist() -> TestResult<()> { // check that the default author exists and cannot be deleted. let default_author = { let iroh = Node::persistent(iroh_root).spawn().await?; - let author = iroh.authors().default().await?; - assert!(iroh.authors().export(author).await?.is_some()); - assert!(iroh.authors().delete(author).await.is_err()); + let author = iroh.docs().author_default().await?; + assert!(iroh.docs().author_export(author).await?.is_some()); + assert!(iroh.docs().author_delete(author).await.is_err()); iroh.shutdown().await?; author }; @@ -173,10 +184,10 @@ async fn test_default_author_persist() -> TestResult<()> { // check that the default author is persisted across restarts. { let iroh = Node::persistent(iroh_root).spawn().await?; - let author = iroh.authors().default().await?; + let author = iroh.docs().author_default().await?; assert_eq!(author, default_author); - assert!(iroh.authors().export(author).await?.is_some()); - assert!(iroh.authors().delete(author).await.is_err()); + assert!(iroh.docs().author_export(author).await?.is_some()); + assert!(iroh.docs().author_delete(author).await.is_err()); iroh.shutdown().await?; }; @@ -185,10 +196,10 @@ async fn test_default_author_persist() -> TestResult<()> { let default_author = { tokio::fs::remove_file(iroh_root.join("default-author")).await?; let iroh = Node::persistent(iroh_root).spawn().await?; - let author = iroh.authors().default().await?; + let author = iroh.docs().author_default().await?; assert!(author != default_author); - assert!(iroh.authors().export(author).await?.is_some()); - assert!(iroh.authors().delete(author).await.is_err()); + assert!(iroh.docs().author_export(author).await?.is_some()); + assert!(iroh.docs().author_delete(author).await.is_err()); iroh.shutdown().await?; author }; @@ -211,7 +222,7 @@ async fn test_default_author_persist() -> TestResult<()> { drop(iroh); let iroh = Node::persistent(iroh_root).spawn().await; if let Err(cause) = iroh.as_ref() { - panic!("failed to start node: {:?}", cause); + panic!("failed to start node: {cause:?}"); } iroh?.shutdown().await?; } @@ -219,15 +230,15 @@ async fn test_default_author_persist() -> TestResult<()> { // check that the default author can be set manually and is persisted. let default_author = { let iroh = Node::persistent(iroh_root).spawn().await?; - let author = iroh.authors().create().await?; - iroh.authors().set_default(author).await?; - assert_eq!(iroh.authors().default().await?, author); + let author = iroh.docs().author_create().await?; + iroh.docs().author_set_default(author).await?; + assert_eq!(iroh.docs().author_default().await?, author); iroh.shutdown().await?; author }; { let iroh = Node::persistent(iroh_root).spawn().await?; - assert_eq!(iroh.authors().default().await?, default_author); + assert_eq!(iroh.docs().author_default().await?, default_author); iroh.shutdown().await?; } diff --git a/tests/gc.rs b/tests/gc.rs index aca7716..1f9353a 100644 --- a/tests/gc.rs +++ b/tests/gc.rs @@ -1,19 +1,9 @@ -#![cfg(feature = "rpc")] -use std::{ - io::{Cursor, Write}, - path::PathBuf, - time::Duration, -}; +use std::{path::PathBuf, time::Duration}; use anyhow::Result; -use bao_tree::{blake3, io::sync::Outboard, ChunkRanges}; use bytes::Bytes; use futures_lite::StreamExt; -use iroh_blobs::{ - store::{bao_tree, Map}, - IROH_BLOCK_SIZE, -}; -use iroh_io::AsyncSliceReaderExt; +use iroh_blobs::api::blobs::ImportMode; use rand::RngCore; use testdir::testdir; use util::Node; @@ -27,27 +17,11 @@ pub fn create_test_data(size: usize) -> Bytes { res.into() } -/// Take some data and encode it -pub fn simulate_remote(data: &[u8]) -> (blake3::Hash, Cursor) { - let outboard = bao_tree::io::outboard::PostOrderMemOutboard::create(data, IROH_BLOCK_SIZE); - let mut encoded = Vec::new(); - encoded - .write_all(outboard.tree.size().to_le_bytes().as_ref()) - .unwrap(); - bao_tree::io::sync::encode_ranges_validated(data, &outboard, &ChunkRanges::all(), &mut encoded) - .unwrap(); - let hash = outboard.root(); - (hash, Cursor::new(encoded.into())) -} - /// Wrap a bao store in a node that has gc enabled. async fn persistent_node( path: PathBuf, gc_period: Duration, -) -> ( - Node, - async_channel::Receiver<()>, -) { +) -> (Node, async_channel::Receiver<()>) { let (gc_send, gc_recv) = async_channel::unbounded(); let node = Node::persistent(path) .gc_interval(Some(gc_period)) @@ -65,22 +39,24 @@ async fn redb_doc_import_stress() -> Result<()> { let _ = tracing_subscriber::fmt::try_init(); let dir = testdir!(); let (node, _) = persistent_node(dir.join("store"), Duration::from_secs(10)).await; - let bao_store = node.blob_store().clone(); + let blobs = node.blobs().clone(); let client = node.client(); let doc = client.docs().create().await?; - let author = client.authors().create().await?; + let author = client.docs().author_create().await?; let temp_path = dir.join("temp"); tokio::fs::create_dir_all(&temp_path).await?; let mut to_import = Vec::new(); for i in 0..100 { let data = create_test_data(16 * 1024 * 3 + 1); - let path = temp_path.join(format!("file{}", i)); + let path = temp_path.join(format!("file{i}")); tokio::fs::write(&path, &data).await?; let key = Bytes::from(format!("{}", path.display())); to_import.push((key, path, data)); } for (key, path, _) in to_import.iter() { - let mut progress = doc.import_file(author, key.clone(), path, true).await?; + let mut progress = doc + .import_file(&blobs, author, key.clone(), path, ImportMode::TryReference) + .await?; while let Some(msg) = progress.next().await { tracing::info!("import progress {:?}", msg); } @@ -90,10 +66,10 @@ async fn redb_doc_import_stress() -> Result<()> { anyhow::bail!("doc entry not found {}", i); }; let hash = entry.content_hash(); - let Some(content) = bao_store.get(&hash).await? else { + if !blobs.has(hash).await? { anyhow::bail!("content not found {} {}", i, &hash.to_hex()[..8]); }; - let data = content.data_reader().read_to_end().await?; + let data = blobs.get_bytes(hash).await?; assert_eq!(data, expected); } Ok(()) diff --git a/tests/sync.rs b/tests/sync.rs index d06cd97..5d5b42d 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -1,4 +1,3 @@ -#![cfg(feature = "rpc")] use std::{ collections::HashMap, future::Future, @@ -13,14 +12,16 @@ use futures_util::{FutureExt, StreamExt, TryStreamExt}; use iroh::{PublicKey, RelayMode, SecretKey}; use iroh_blobs::Hash; use iroh_docs::{ - rpc::{ - client::docs::{Doc, Entry, LiveEvent, ShareMode}, - AddrInfoOptions, + api::{ + protocol::{AddrInfoOptions, ShareMode}, + Doc, }, + engine::LiveEvent, store::{DownloadPolicy, FilterKind, Query}, - AuthorId, ContentStatus, + AuthorId, ContentStatus, Entry, }; use rand::{CryptoRng, Rng, SeedableRng}; +use tempfile::tempdir; use tracing::{debug, error_span, info, Instrument}; use tracing_test::traced_test; mod util; @@ -28,7 +29,7 @@ use util::{Builder, Node}; const TIMEOUT: Duration = Duration::from_secs(60); -fn test_node(secret_key: SecretKey) -> Builder { +fn test_node(secret_key: SecretKey) -> Builder { Node::memory() .secret_key(secret_key) .relay_mode(RelayMode::Disabled) @@ -40,7 +41,7 @@ fn test_node(secret_key: SecretKey) -> Builder { fn spawn_node( i: usize, rng: &mut (impl CryptoRng + Rng), -) -> impl Future>> + 'static { +) -> impl Future> + 'static { let secret_key = SecretKey::generate(rng); async move { let node = test_node(secret_key); @@ -50,10 +51,7 @@ fn spawn_node( } } -async fn spawn_nodes( - n: usize, - mut rng: &mut (impl CryptoRng + Rng), -) -> anyhow::Result>> { +async fn spawn_nodes(n: usize, mut rng: &mut (impl CryptoRng + Rng)) -> anyhow::Result> { let mut futs = vec![]; for i in 0..n { futs.push(spawn_node(i, &mut rng)); @@ -81,7 +79,7 @@ async fn sync_simple() -> Result<()> { // create doc on node0 let peer0 = nodes[0].node_id(); - let author0 = clients[0].authors().create().await?; + let author0 = clients[0].docs().author_create().await?; let doc0 = clients[0].docs().create().await?; let blobs0 = clients[0].blobs(); let hash0 = doc0 @@ -140,7 +138,7 @@ async fn sync_subscribe_no_sync() -> Result<()> { let client = node.client(); let doc = client.docs().create().await?; let mut sub = doc.subscribe().await?; - let author = client.authors().create().await?; + let author = client.docs().author_create().await?; doc.set_bytes(author, b"k".to_vec(), b"v".to_vec()).await?; let event = tokio::time::timeout(Duration::from_millis(100), sub.next()).await?; assert!( @@ -163,7 +161,7 @@ async fn sync_gossip_bulk() -> Result<()> { let clients = nodes.iter().map(|node| node.client()).collect::>(); let _peer0 = nodes[0].node_id(); - let author0 = clients[0].authors().create().await?; + let author0 = clients[0].docs().author_create().await?; let doc0 = clients[0].docs().create().await?; let mut ticket = doc0 .share(ShareMode::Write, AddrInfoOptions::RelayAndAddresses) @@ -254,7 +252,7 @@ async fn sync_full_basic() -> testresult::TestResult<()> { // peer0: create doc and ticket let peer0 = nodes[0].node_id(); - let author0 = clients[0].authors().create().await?; + let author0 = clients[0].docs().author_create().await?; let doc0 = clients[0].docs().create().await?; let blobs0 = clients[0].blobs(); let mut events0 = doc0.subscribe().await?; @@ -277,7 +275,7 @@ async fn sync_full_basic() -> testresult::TestResult<()> { info!("peer1: spawn"); let peer1 = nodes[1].node_id(); - let author1 = clients[1].authors().create().await?; + let author1 = clients[1].docs().author_create().await?; info!("peer1: join doc"); let doc1 = clients[1].docs().import(ticket.clone()).await?; let blobs1 = clients[1].blobs(); @@ -455,7 +453,7 @@ async fn sync_subscribe_stop_close() -> Result<()> { let client = node.client(); let doc = client.docs().create().await?; - let author = client.authors().create().await?; + let author = client.docs().author_create().await?; let status = doc.status().await?; assert_eq!(status.subscribers, 0); @@ -488,7 +486,6 @@ async fn sync_subscribe_stop_close() -> Result<()> { #[tokio::test] #[traced_test] -#[cfg(feature = "test-utils")] async fn test_sync_via_relay() -> Result<()> { let (relay_map, _relay_url, _guard) = iroh::test_utils::run_relay_server().await?; @@ -506,7 +503,7 @@ async fn test_sync_via_relay() -> Result<()> { .await?; let doc1 = node1.docs().create().await?; - let author1 = node1.authors().create().await?; + let author1 = node1.docs().author_create().await?; let inserted_hash = doc1 .set_bytes(author1, b"foo".to_vec(), b"bar".to_vec()) .await?; @@ -529,7 +526,7 @@ async fn test_sync_via_relay() -> Result<()> { Box::new(move |e| matches!(e, LiveEvent::NeighborUp(n) if *n== node1_id)), Box::new(move |e| match_sync_finished(e, node1_id)), Box::new( - move |e| matches!(e, LiveEvent::InsertRemote { from, content_status: ContentStatus::Missing, .. } if *from == node1_id), + move |e| matches!(e, LiveEvent::InsertRemote { from, content_status: ContentStatus::Missing | ContentStatus::Incomplete, .. } if *from == node1_id), ), Box::new( move |e| matches!(e, LiveEvent::ContentReady { hash } if *hash == inserted_hash), @@ -539,7 +536,7 @@ async fn test_sync_via_relay() -> Result<()> { vec![Box::new(move |e| match_sync_finished(e, node1_id))], ).await; let actual = blobs2 - .read_to_bytes( + .get_bytes( doc2.get_exact(author1, b"foo", false) .await? .expect("entry to exist") @@ -557,7 +554,7 @@ async fn test_sync_via_relay() -> Result<()> { Duration::from_secs(2), vec![ Box::new( - move |e| matches!(e, LiveEvent::InsertRemote { from, content_status: ContentStatus::Missing, .. } if *from == node1_id), + move |e| matches!(e, LiveEvent::InsertRemote { from, content_status: ContentStatus::Missing | ContentStatus::Incomplete, .. } if *from == node1_id), ), Box::new( move |e| matches!(e, LiveEvent::ContentReady { hash } if *hash == updated_hash), @@ -569,7 +566,7 @@ async fn test_sync_via_relay() -> Result<()> { ], ).await; let actual = blobs2 - .read_to_bytes( + .get_bytes( doc2.get_exact(author1, b"foo", false) .await? .expect("entry to exist") @@ -582,7 +579,6 @@ async fn test_sync_via_relay() -> Result<()> { #[tokio::test] #[traced_test] -#[cfg(feature = "test-utils")] #[ignore = "flaky"] async fn sync_restart_node() -> Result<()> { let mut rng = test_rng(b"sync_restart_node"); @@ -622,7 +618,7 @@ async fn sync_restart_node() -> Result<()> { .spawn() .await?; let id2 = node2.node_id(); - let author2 = node2.authors().create().await?; + let author2 = node2.docs().author_create().await?; let doc2 = node2.docs().import(ticket.clone()).await?; let blobs2 = node2.blobs(); @@ -756,13 +752,13 @@ async fn test_download_policies() -> Result<()> { let clients = nodes.iter().map(|node| node.client()).collect::>(); let doc_a = clients[0].docs().create().await?; - let author_a = clients[0].authors().create().await?; + let author_a = clients[0].docs().author_create().await?; let ticket = doc_a .share(ShareMode::Write, AddrInfoOptions::RelayAndAddresses) .await?; let doc_b = clients[1].docs().import(ticket).await?; - let author_b = clients[1].authors().create().await?; + let author_b = clients[1].docs().author_create().await?; doc_a.set_download_policy(policy_a).await?; doc_b.set_download_policy(policy_b).await?; @@ -878,7 +874,7 @@ async fn sync_big() -> Result<()> { let nodes = spawn_nodes(n_nodes, &mut rng).await?; let node_ids = nodes.iter().map(|node| node.node_id()).collect::>(); let clients = nodes.iter().map(|node| node.client()).collect::>(); - let authors = collect_futures(clients.iter().map(|c| c.authors().create())).await?; + let authors = collect_futures(clients.iter().map(|c| c.docs().author_create())).await?; let doc0 = clients[0].docs().create().await?; let mut ticket = doc0 @@ -982,7 +978,6 @@ async fn sync_big() -> Result<()> { #[tokio::test] #[traced_test] -#[cfg(feature = "test-utils")] async fn test_list_docs_stream() -> testresult::TestResult<()> { let node = Node::memory() .relay_mode(RelayMode::Disabled) @@ -1027,13 +1022,13 @@ async fn get_all(doc: &Doc) -> anyhow::Result> { /// Get all entries of a document with the blob content. async fn get_all_with_content( - blobs: &iroh_blobs::rpc::client::blobs::Client, + blobs: &iroh_blobs::api::Store, doc: &Doc, ) -> anyhow::Result> { let entries = doc.get_many(Query::all()).await?; let entries = entries.and_then(|entry| async { let hash = entry.content_hash(); - let content = blobs.read_to_bytes(hash).await; + let content = blobs.get_bytes(hash).await.map_err(anyhow::Error::from); content.map(|c| (entry, c)) }); let entries = entries.collect::>().await; @@ -1158,14 +1153,17 @@ impl PartialEq for (Entry, Bytes) { #[tokio::test] #[traced_test] async fn doc_delete() -> Result<()> { - let node = Node::memory() + let tempdir = tempdir()?; + // TODO(Frando): iroh-blobs has gc only for fs store atm, change test to test both + // mem and persistent once this changes. + let node = Node::persistent(tempdir.path()) .gc_interval(Some(Duration::from_millis(100))) .spawn() .await?; let client = node.client(); let doc = client.docs().create().await?; let blobs = client.blobs(); - let author = client.authors().create().await?; + let author = client.docs().author_create().await?; let hash = doc .set_bytes(author, b"foo".to_vec(), b"hi".to_vec()) .await?; @@ -1178,8 +1176,8 @@ async fn doc_delete() -> Result<()> { // wait for gc // TODO: allow to manually trigger gc - tokio::time::sleep(Duration::from_millis(200)).await; - let bytes = client.blobs().read_to_bytes(hash).await; + tokio::time::sleep(Duration::from_millis(400)).await; + let bytes = client.blobs().get_bytes(hash).await; assert!(bytes.is_err()); node.shutdown().await?; Ok(()) @@ -1193,7 +1191,7 @@ async fn sync_drop_doc() -> Result<()> { let client = node.client(); let doc = client.docs().create().await?; - let author = client.authors().create().await?; + let author = client.docs().author_create().await?; let mut sub = doc.subscribe().await?; doc.set_bytes(author, b"foo".to_vec(), b"bar".to_vec()) @@ -1216,29 +1214,24 @@ async fn sync_drop_doc() -> Result<()> { Ok(()) } -async fn assert_latest( - blobs: &iroh_blobs::rpc::client::blobs::Client, - doc: &Doc, - key: &[u8], - value: &[u8], -) { +async fn assert_latest(blobs: &iroh_blobs::api::Store, doc: &Doc, key: &[u8], value: &[u8]) { let content = get_latest(blobs, doc, key).await.unwrap(); assert_eq!(content, value.to_vec()); } async fn get_latest( - blobs: &iroh_blobs::rpc::client::blobs::Client, + blobs: &iroh_blobs::api::Store, doc: &Doc, key: &[u8], ) -> anyhow::Result> { let query = Query::single_latest_per_key().key_exact(key); - let entry = doc - .get_many(query) - .await? + let stream = doc.get_many(query).await?; + tokio::pin!(stream); + let entry = stream .next() .await .ok_or_else(|| anyhow!("entry not found"))??; - let content = blobs.read_to_bytes(entry.content_hash()).await?; + let content = blobs.get_bytes(entry.content_hash()).await?; Ok(content.to_vec()) } diff --git a/tests/util.rs b/tests/util.rs index 5f83e78..c605a36 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -1,23 +1,14 @@ -#![cfg(feature = "rpc")] #![allow(dead_code)] use std::{ - marker::PhantomData, net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}, ops::Deref, path::{Path, PathBuf}, }; -use iroh::{discovery::Discovery, dns::DnsResolver, NodeId, RelayMode, SecretKey}; -use iroh_blobs::{ - net_protocol::Blobs, - store::{GcConfig, Store as BlobStore}, -}; -use iroh_docs::protocol::Docs; +use iroh::{discovery::IntoDiscovery, dns::DnsResolver, NodeId, RelayMode, SecretKey}; +use iroh_blobs::store::fs::options::{GcConfig, Options}; +use iroh_docs::{engine::ProtectCallbackHandler, protocol::Docs}; use iroh_gossip::net::Gossip; -use nested_enum_utils::enum_conversions; -use quic_rpc::transport::{Connector, Listener}; -use serde::{Deserialize, Serialize}; -use tokio_util::task::AbortOnDropHandle; /// Default bind address for the node. /// 11204 is "iroh" in leetspeak @@ -33,14 +24,14 @@ pub const DEFAULT_BIND_ADDR_V6: SocketAddrV6 = /// An iroh node that just has the blobs transport #[derive(Debug)] -pub struct Node { +pub struct Node { router: iroh::protocol::Router, client: Client, - store: S, - rpc_task: AbortOnDropHandle<()>, + // store: iroh_blobs::api::Store, + // rpc_task: AbortOnDropHandle<()>, } -impl Deref for Node { +impl Deref for Node { type Target = Client; fn deref(&self) -> &Self::Target { @@ -48,191 +39,123 @@ impl Deref for Node { } } -#[derive(Debug, Serialize, Deserialize)] -#[enum_conversions] -enum Request { - BlobsOrTags(iroh_blobs::rpc::proto::Request), - Docs(iroh_docs::rpc::proto::Request), -} - -#[derive(Debug, Serialize, Deserialize)] -#[enum_conversions] -enum Response { - BlobsOrTags(iroh_blobs::rpc::proto::Response), - Docs(iroh_docs::rpc::proto::Response), -} - -#[derive(Debug, Clone, Copy)] -struct Service; - -impl quic_rpc::Service for Service { - type Req = Request; - type Res = Response; -} - #[derive(Debug, Clone)] pub struct Client { - blobs: iroh_blobs::rpc::client::blobs::Client, - docs: iroh_docs::rpc::client::docs::Client, - authors: iroh_docs::rpc::client::authors::Client, + blobs: iroh_blobs::api::Store, + docs: iroh_docs::api::DocsApi, } impl Client { - fn new(client: quic_rpc::RpcClient) -> Self { - Self { - blobs: iroh_blobs::rpc::client::blobs::Client::new(client.clone().map().boxed()), - docs: iroh_docs::rpc::client::docs::Client::new(client.clone().map().boxed()), - authors: iroh_docs::rpc::client::authors::Client::new(client.map().boxed()), - } + fn new(blobs: iroh_blobs::api::Store, docs: iroh_docs::api::DocsApi) -> Self { + Self { blobs, docs } } - pub fn blobs(&self) -> &iroh_blobs::rpc::client::blobs::Client { + pub fn blobs(&self) -> &iroh_blobs::api::Store { &self.blobs } - pub fn docs(&self) -> &iroh_docs::rpc::client::docs::Client { + pub fn docs(&self) -> &iroh_docs::api::DocsApi { &self.docs } - - pub fn authors(&self) -> &iroh_docs::rpc::client::authors::Client { - &self.authors - } } /// An iroh node builder #[derive(derive_more::Debug)] -pub struct Builder { +pub struct Builder { + endpoint: iroh::endpoint::Builder, + use_n0_discovery: bool, path: Option, - secret_key: Option, - relay_mode: RelayMode, - dns_resolver: Option, - node_discovery: Option>, + // node_discovery: Option>, gc_interval: Option, #[debug(skip)] register_gc_done_cb: Option>, - insecure_skip_relay_cert_verify: bool, bind_random_port: bool, - _p: PhantomData, } -impl Builder { +impl Builder { /// Spawns the node - async fn spawn0(self, store: S) -> anyhow::Result> { + async fn spawn0( + self, + blobs: iroh_blobs::api::Store, + protect_cb: Option, + ) -> anyhow::Result { let mut addr_v4 = DEFAULT_BIND_ADDR_V4; let mut addr_v6 = DEFAULT_BIND_ADDR_V6; if self.bind_random_port { addr_v4.set_port(0); addr_v6.set_port(0); } - let mut builder = iroh::Endpoint::builder() - .bind_addr_v4(addr_v4) - .bind_addr_v6(addr_v6) - .relay_mode(self.relay_mode.clone()) - .insecure_skip_relay_cert_verify(self.insecure_skip_relay_cert_verify); - if let Some(dns_resolver) = self.dns_resolver.clone() { - builder = builder.dns_resolver(dns_resolver); - } - if let Some(secret_key) = self.secret_key { - builder = builder.secret_key(secret_key); - } - if let Some(discovery) = self.node_discovery { - builder = builder.discovery(discovery); - } else { + let mut builder = self.endpoint.bind_addr_v4(addr_v4).bind_addr_v6(addr_v6); + if self.use_n0_discovery { builder = builder.discovery_n0(); } + builder = builder.discovery_n0(); let endpoint = builder.bind().await?; let mut router = iroh::protocol::Router::builder(endpoint.clone()); - let blobs = Blobs::builder(store.clone()).build(&endpoint); - let gossip = Gossip::builder().spawn(endpoint.clone()).await?; - let builder = match self.path { + let gossip = Gossip::builder().spawn(endpoint.clone()); + let mut docs_builder = match self.path { Some(ref path) => Docs::persistent(path.to_path_buf()), None => Docs::memory(), }; - let docs = match builder.spawn(&blobs, &gossip).await { + if let Some(protect_cb) = protect_cb { + docs_builder = docs_builder.protect_handler(protect_cb); + } + let docs = match docs_builder + .spawn(endpoint.clone(), blobs.clone(), gossip.clone()) + .await + { Ok(docs) => docs, Err(err) => { - store.shutdown().await; + blobs.shutdown().await.ok(); return Err(err); } }; - router = router.accept(iroh_blobs::ALPN, blobs.clone()); + router = router.accept( + iroh_blobs::ALPN, + iroh_blobs::net_protocol::Blobs::new(&blobs, endpoint.clone(), None), + ); router = router.accept(iroh_docs::ALPN, docs.clone()); router = router.accept(iroh_gossip::ALPN, gossip.clone()); // Build the router let router = router.spawn(); - // Setup RPC - let (internal_rpc, controller) = - quic_rpc::transport::flume::channel::(1); - let controller = controller.boxed(); - let internal_rpc = internal_rpc.boxed(); - let internal_rpc = quic_rpc::RpcServer::::new(internal_rpc); - - let docs2 = docs.clone(); - let blobs2 = blobs.clone(); - let rpc_task: tokio::task::JoinHandle<()> = tokio::task::spawn(async move { - loop { - let request = internal_rpc.accept().await; - match request { - Ok(accepting) => { - let blobs = blobs2.clone(); - let docs = docs2.clone(); - tokio::task::spawn(async move { - let (msg, chan) = accepting.read_first().await?; - match msg { - Request::BlobsOrTags(msg) => { - blobs.handle_rpc_request(msg, chan.map().boxed()).await?; - } - Request::Docs(msg) => { - docs.handle_rpc_request(msg, chan.map().boxed()).await?; - } - } - anyhow::Ok(()) - }); - } - Err(err) => { - tracing::warn!("rpc error: {:?}", err); - } - } - } - }); - - let client = quic_rpc::RpcClient::new(controller); - if let Some(period) = self.gc_interval { - blobs.add_protected(docs.protect_cb())?; - blobs.start_gc(GcConfig { - period, - done_callback: self.register_gc_done_cb, - })?; - } + // TODO: Make this work again. + // if let Some(period) = self.gc_interval { + // blobs.add_protected(docs.protect_cb())?; + // blobs.start_gc(GcConfig { + // period, + // done_callback: self.register_gc_done_cb, + // })?; + // } - let client = Client::new(client); + let client = Client::new(blobs.clone(), docs.api().clone()); Ok(Node { router, client, - store, - rpc_task: AbortOnDropHandle::new(rpc_task), + // store: blobs, + // rpc_task: AbortOnDropHandle::new(rpc_task), }) } pub fn secret_key(mut self, value: SecretKey) -> Self { - self.secret_key = Some(value); + self.endpoint = self.endpoint.secret_key(value); self } pub fn relay_mode(mut self, value: RelayMode) -> Self { - self.relay_mode = value; + self.endpoint = self.endpoint.relay_mode(value); self } pub fn dns_resolver(mut self, value: DnsResolver) -> Self { - self.dns_resolver = Some(value); + self.endpoint = self.endpoint.dns_resolver(value); self } - pub fn node_discovery(mut self, value: Box) -> Self { - self.node_discovery = Some(value); + pub fn node_discovery(mut self, value: impl IntoDiscovery) -> Self { + self.use_n0_discovery = false; + self.endpoint = self.endpoint.discovery(value); self } @@ -247,7 +170,7 @@ impl Builder { } pub fn insecure_skip_relay_cert_verify(mut self, value: bool) -> Self { - self.insecure_skip_relay_cert_verify = value; + self.endpoint = self.endpoint.insecure_skip_relay_cert_verify(value); self } @@ -258,66 +181,75 @@ impl Builder { fn new(path: Option) -> Self { Self { + endpoint: iroh::Endpoint::builder(), + use_n0_discovery: true, path, - secret_key: None, - relay_mode: RelayMode::Default, gc_interval: None, - insecure_skip_relay_cert_verify: false, bind_random_port: false, - dns_resolver: None, - node_discovery: None, + // node_discovery: None, register_gc_done_cb: None, - _p: PhantomData, + // _p: PhantomData, } } } -impl Node { +impl Node { /// Creates a new node with memory storage - pub fn memory() -> Builder { + pub fn memory() -> Builder { Builder::new(None) } -} -impl Builder { - /// Spawns the node - pub async fn spawn(self) -> anyhow::Result> { - let store = iroh_blobs::store::mem::Store::new(); - self.spawn0(store).await - } -} - -impl Node { /// Creates a new node with persistent storage - pub fn persistent(path: impl AsRef) -> Builder { + pub fn persistent(path: impl AsRef) -> Builder { let path = Some(path.as_ref().to_owned()); Builder::new(path) } } -impl Builder { +impl Builder { /// Spawns the node - pub async fn spawn(self) -> anyhow::Result> { - let store = iroh_blobs::store::fs::Store::load(self.path.clone().unwrap()).await?; - self.spawn0(store).await + pub async fn spawn(self) -> anyhow::Result { + let (store, protect_handler) = match self.path { + None => { + let store = iroh_blobs::store::mem::MemStore::new(); + ((*store).clone(), None) + } + Some(ref path) => { + let db_path = path.join("blobs.db"); + let mut opts = Options::new(path); + let protect_handler = if let Some(interval) = self.gc_interval { + let (handler, cb) = ProtectCallbackHandler::new(); + opts.gc = Some(GcConfig { + interval, + add_protected: Some(cb), + }); + Some(handler) + } else { + None + }; + let store = iroh_blobs::store::fs::FsStore::load_with_opts(db_path, opts).await?; + ((*store).clone(), protect_handler) + } + }; + self.spawn0(store, protect_handler).await } } -impl Node { +impl Node { /// Returns the node id pub fn node_id(&self) -> NodeId { self.router.endpoint().node_id() } - /// Returns the blob store - pub fn blob_store(&self) -> &S { - &self.store - } + // /// Returns the blob store + // pub fn blob_store(&self) -> &S { + // &self.store + // } /// Shuts down the node pub async fn shutdown(self) -> anyhow::Result<()> { self.router.shutdown().await?; - self.rpc_task.abort(); + // self.rpc_task.abort(); Ok(()) } @@ -326,3 +258,134 @@ impl Node { &self.client } } + +pub mod path { + use std::path::{Component, Path, PathBuf}; + + use anyhow::Context; + use bytes::Bytes; + + /// Helper function that translates a key that was derived from the [`path_to_key`] function back + /// into a path. + /// + /// If `prefix` exists, it will be stripped before converting back to a path + /// If `root` exists, will add the root as a parent to the created path + /// Removes any null byte that has been appended to the key + pub fn key_to_path( + key: impl AsRef<[u8]>, + prefix: Option, + root: Option, + ) -> anyhow::Result { + let mut key = key.as_ref(); + if key.is_empty() { + return Ok(PathBuf::new()); + } + // if the last element is the null byte, remove it + if b'\0' == key[key.len() - 1] { + key = &key[..key.len() - 1] + } + + let key = if let Some(prefix) = prefix { + let prefix = prefix.into_bytes(); + if prefix[..] == key[..prefix.len()] { + &key[prefix.len()..] + } else { + anyhow::bail!("key {:?} does not begin with prefix {:?}", key, prefix); + } + } else { + key + }; + + let mut path = if key[0] == b'/' { + PathBuf::from("/") + } else { + PathBuf::new() + }; + for component in key + .split(|c| c == &b'/') + .map(|c| String::from_utf8(c.into()).context("key contains invalid data")) + { + let component = component?; + path = path.join(component); + } + + // add root if it exists + let path = if let Some(root) = root { + root.join(path) + } else { + path + }; + + Ok(path) + } + + /// Helper function that creates a document key from a canonicalized path, removing the `root` and adding the `prefix`, if they exist + /// + /// Appends the null byte to the end of the key. + pub fn path_to_key( + path: impl AsRef, + prefix: Option, + root: Option, + ) -> anyhow::Result { + let path = path.as_ref(); + let path = if let Some(root) = root { + path.strip_prefix(root)? + } else { + path + }; + let suffix = canonicalized_path_to_string(path, false)?.into_bytes(); + let mut key = if let Some(prefix) = prefix { + prefix.into_bytes().to_vec() + } else { + Vec::new() + }; + key.extend(suffix); + key.push(b'\0'); + Ok(key.into()) + } + + /// This function converts an already canonicalized path to a string. + /// + /// If `must_be_relative` is true, the function will fail if any component of the path is + /// `Component::RootDir` + /// + /// This function will also fail if the path is non canonical, i.e. contains + /// `..` or `.`, or if the path components contain any windows or unix path + /// separators. + pub fn canonicalized_path_to_string( + path: impl AsRef, + must_be_relative: bool, + ) -> anyhow::Result { + let mut path_str = String::new(); + let parts = path + .as_ref() + .components() + .filter_map(|c| match c { + Component::Normal(x) => { + let c = match x.to_str() { + Some(c) => c, + None => return Some(Err(anyhow::anyhow!("invalid character in path"))), + }; + + if !c.contains('/') && !c.contains('\\') { + Some(Ok(c)) + } else { + Some(Err(anyhow::anyhow!("invalid path component {:?}", c))) + } + } + Component::RootDir => { + if must_be_relative { + Some(Err(anyhow::anyhow!("invalid path component {:?}", c))) + } else { + path_str.push('/'); + None + } + } + _ => Some(Err(anyhow::anyhow!("invalid path component {:?}", c))), + }) + .collect::>>()?; + let parts = parts.join("/"); + path_str.push_str(&parts); + Ok(path_str) + } +}