diff --git a/Cargo.lock b/Cargo.lock index 01239ca..61ec0c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,37 +1,22 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "addr2line" -version = "0.22.0" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -43,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -58,38 +43,45 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "async-channel" version = "1.9.0" @@ -103,9 +95,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -115,14 +107,15 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.0", - "futures-lite 2.3.0", + "fastrand", + "futures-lite", + "pin-project-lite", "slab", ] @@ -132,70 +125,40 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-executor", - "async-io 2.3.3", - "async-lock 3.4.0", + "async-io", + "async-lock", "blocking", - "futures-lite 2.3.0", + "futures-lite", "once_cell", ] [[package]] name = "async-io" -version = "1.13.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock 2.8.0", "autocfg", "cfg-if", "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" -dependencies = [ - "async-lock 3.4.0", - "cfg-if", - "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "parking", - "polling 3.7.2", - "rustix 0.38.34", + "polling", + "rustix", "slab", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", + "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.0" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.3.1", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] @@ -214,19 +177,19 @@ dependencies = [ [[package]] name = "async-std" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ "async-channel 1.9.0", "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", + "async-io", + "async-lock", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 1.13.0", + "futures-lite", "gloo-timers", "kv-log-macro", "log", @@ -246,9 +209,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -263,24 +226,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "backtrace" -version = "0.3.73" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -290,15 +238,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.6.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -320,22 +262,22 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "piper", ] [[package]] name = "bstr" -version = "1.10.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "serde", @@ -343,52 +285,49 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" -version = "1.6.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.1.7" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "clap" -version = "4.5.11" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -396,9 +335,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.11" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -410,9 +349,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.11" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -422,15 +361,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -443,22 +382,22 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", - "lazy_static", "libc", + "once_cell", "unicode-width", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -466,33 +405,33 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -509,15 +448,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -573,6 +512,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -581,24 +531,24 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -609,9 +559,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -620,34 +570,31 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.3.1", + "event-listener 5.4.1", "pin-project-lite", ] [[package]] name = "fastrand" -version = "1.9.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "fastrand" -version = "2.1.0" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.0.30" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -659,6 +606,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -676,18 +629,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -700,9 +653,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -710,15 +663,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -727,32 +680,17 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-lite" -version = "1.13.0" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "fastrand 2.1.0", + "fastrand", "futures-core", "futures-io", "parking", @@ -761,9 +699,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -772,21 +710,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -796,7 +734,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -812,9 +749,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -822,16 +759,23 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.29.0" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] [[package]] name = "globset" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -842,9 +786,9 @@ dependencies = [ [[package]] name = "gloo-timers" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", @@ -854,9 +798,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -873,9 +817,18 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -885,15 +838,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -935,9 +882,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -947,9 +894,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -962,7 +909,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.7", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -984,14 +931,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1005,21 +953,119 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" -version = "0.5.0" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", ] [[package]] name = "ignore" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", @@ -1033,65 +1079,48 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] name = "indicatif" -version = "0.17.8" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", "unicode-width", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", + "web-time", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1106,9 +1135,9 @@ dependencies = [ [[package]] name = "lazy-regex" -version = "3.2.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576c8060ecfdf2e56995cf3274b4f2d71fa5e4fa3607c1c0b63c10180ee58741" +checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" dependencies = [ "lazy-regex-proc_macros", "once_cell", @@ -1117,9 +1146,9 @@ dependencies = [ [[package]] name = "lazy-regex-proc_macros" -version = "3.2.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9efb9e65d4503df81c615dc33ff07042a9408ac7f26b45abee25566f7fbfd12c" +checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" dependencies = [ "proc-macro2", "quote", @@ -1133,27 +1162,32 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.155" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.6.0", "libc", ] [[package]] name = "libssh2-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" dependencies = [ "cc", "libc", @@ -1165,9 +1199,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.18" +version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" dependencies = [ "cc", "libc", @@ -1177,31 +1211,30 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] -name = "linux-raw-sys" -version = "0.4.14" +name = "litemap" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ "value-bag", ] @@ -1219,36 +1252,36 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -1272,11 +1305,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -1287,19 +1320,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "object" -version = "0.36.2" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" -dependencies = [ - "memchr", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "once_cell" -version = "1.19.0" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -1309,11 +1339,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.6.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -1335,15 +1365,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -1353,78 +1383,53 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - -[[package]] -name = "parking_lot" -version = "0.11.2" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -1433,9 +1438,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1445,86 +1450,93 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand", "futures-io", ] [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" -version = "2.8.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ - "autocfg", - "bitflags 1.3.2", "cfg-if", "concurrent-queue", - "libc", - "log", + "hermit-abi", "pin-project-lite", - "windows-sys 0.48.0", + "rustix", + "windows-sys 0.61.2", ] [[package]] -name = "polling" -version = "3.7.2" +name = "portable-atomic" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.4.0", - "pin-project-lite", - "rustix 0.38.34", - "tracing", - "windows-sys 0.52.0", + "zerovec", ] [[package]] -name = "portable-atomic" -version = "1.7.0" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] [[package]] -name = "ppv-lite86" -version = "0.2.19" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2288c0e17cc8d342c712bb43a257a80ebffce59cdb33d5000d8348f3ec02528b" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "zerocopy", - "zerocopy-derive", + "proc-macro2", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -1552,43 +1564,34 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", + "getrandom 0.2.17", ] [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.17", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1598,9 +1601,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1609,9 +1612,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rusoto_core" @@ -1695,53 +1698,33 @@ dependencies = [ "tokio", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.37.27" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 1.3.2", + "bitflags", "errno", - "io-lifetimes", "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.6.0", - "errno", - "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", -] - -[[package]] -name = "ryu" -version = "1.0.18" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -1754,11 +1737,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1769,11 +1752,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.6.0", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -1782,9 +1765,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -1792,24 +1775,34 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1818,14 +1811,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1843,9 +1837,9 @@ dependencies = [ [[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", @@ -1854,14 +1848,14 @@ dependencies = [ [[package]] name = "sha256" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" dependencies = [ "async-trait", "bytes", "hex", - "sha2 0.10.8", + "sha2 0.10.9", "tokio", ] @@ -1873,60 +1867,70 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[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" [[package]] name = "socket2" -version = "0.4.10" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "socket2" -version = "0.5.7" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "ssh2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fe461910559f6d5604c3731d00d2aafc4a83d1665922e280f42f9a168d5455" +checksum = "2f84d13b3b8a0d4e91a2629911e951db1bb8671512f5c09d7d4ba34500ba68c8" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", "libssh2-sys", - "parking_lot 0.11.2", + "parking_lot", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -1949,7 +1953,7 @@ dependencies = [ "async-std", "async-trait", "chrono", - "futures-lite 2.3.0", + "futures-lite", "lazy-regex", "log", "pin-project", @@ -1958,9 +1962,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1969,7 +1973,7 @@ dependencies = [ [[package]] name = "syncbox" -version = "0.5.4" +version = "0.5.6" dependencies = [ "async-trait", "clap", @@ -1990,36 +1994,49 @@ dependencies = [ "sha256", "ssh2", "suppaftp", + "tempfile", "tokio", "tokio-util", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" -version = "3.10.1" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", - "fastrand 2.1.0", - "rustix 0.38.34", - "windows-sys 0.52.0", + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -2027,43 +2044,37 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinystr" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.39.2" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", "libc", "mio", - "parking_lot 0.12.3", + "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2 0.6.3", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -2082,9 +2093,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2096,15 +2107,15 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -2112,9 +2123,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2127,57 +2138,52 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.15" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unicode-normalization" -version = "0.1.23" +name = "unicode-width" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "unicode-width" -version = "0.1.13" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" -version = "2.5.2" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2186,9 +2192,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "value-bag" -version = "1.9.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" [[package]] name = "vcpkg" @@ -2202,12 +2208,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -2229,52 +2229,60 @@ 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 = "wasm-bindgen" -version = "0.2.92" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "bumpalo", - "log", + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2282,28 +2290,75 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -2327,11 +2382,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2342,20 +2397,61 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.48.5", + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] @@ -2364,22 +2460,25 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link", ] [[package]] @@ -2388,46 +2487,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2442,81 +2523,233 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_msvc" +name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_gnu" +name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xml-rs" -version = "0.8.20" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "yoke" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", + "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 45d4d98..04494f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Roman Schejbal "] edition = "2021" name = "syncbox" -version = "0.5.4" +version = "0.5.6" [dependencies] async-trait = "0.1.74" @@ -26,3 +26,6 @@ ssh2 = "0.9.4" suppaftp = {version = "5.2.2", features = ["async-native-tls"]} tokio = {version = "1.34.0", features = ["full"]} tokio-util = {version = "0.7.10", features = ["compat"]} + +[dev-dependencies] +tempfile = "3" diff --git a/src/checksum_tree.rs b/src/checksum_tree.rs index fdc4be5..17f4fef 100644 --- a/src/checksum_tree.rs +++ b/src/checksum_tree.rs @@ -162,7 +162,6 @@ impl DerefMut for ChecksumTree { } #[cfg(test)] - mod tests { use super::*; @@ -231,4 +230,145 @@ mod tests { r#"{"version":"0.3.0","root":{"Directory":{"dirrr":{"Directory":{"DSC05947.ARW":{"File":"a4849b4f83f996ef9ce68b9f8561db4a991ab5f9dce3c52a45267c8e274bb73a"}}}}}}"# ); } + + #[test] + fn from_hashmap_single_file() { + let mut map = HashMap::new(); + map.insert("./file.txt".to_string(), "abc123".to_string()); + let tree: ChecksumTree = map.into(); + + match tree.as_ref().unwrap() { + ChecksumElement::Directory(root) => { + let dot = root.get(".").unwrap(); + match dot { + ChecksumElement::Directory(dir) => match dir.get("file.txt").unwrap() { + ChecksumElement::File(hash) => assert_eq!(hash, "abc123"), + _ => panic!("expected File"), + }, + _ => panic!("expected Directory"), + } + } + _ => panic!("expected Directory"), + } + } + + #[test] + fn from_hashmap_multiple_files_same_directory() { + let mut map = HashMap::new(); + map.insert("./a.txt".to_string(), "hash_a".to_string()); + map.insert("./b.txt".to_string(), "hash_b".to_string()); + let tree: ChecksumTree = map.into(); + + match tree.as_ref().unwrap() { + ChecksumElement::Directory(root) => { + let dot = match root.get(".").unwrap() { + ChecksumElement::Directory(d) => d, + _ => panic!("expected Directory"), + }; + assert_eq!(dot.len(), 2); + assert!( + matches!(dot.get("a.txt"), Some(ChecksumElement::File(h)) if h == "hash_a") + ); + assert!( + matches!(dot.get("b.txt"), Some(ChecksumElement::File(h)) if h == "hash_b") + ); + } + _ => panic!("expected Directory"), + } + } + + #[test] + fn from_hashmap_deeply_nested() { + let mut map = HashMap::new(); + map.insert("./a/b/c/file.txt".to_string(), "deep_hash".to_string()); + let tree: ChecksumTree = map.into(); + + // Navigate: root -> "." -> "a" -> "b" -> "c" -> "file.txt" + let root = match tree.as_ref().unwrap() { + ChecksumElement::Directory(d) => d, + _ => panic!(), + }; + let dot = match root.get(".").unwrap() { + ChecksumElement::Directory(d) => d, + _ => panic!(), + }; + let a = match dot.get("a").unwrap() { + ChecksumElement::Directory(d) => d, + _ => panic!(), + }; + let b = match a.get("b").unwrap() { + ChecksumElement::Directory(d) => d, + _ => panic!(), + }; + let c = match b.get("c").unwrap() { + ChecksumElement::Directory(d) => d, + _ => panic!(), + }; + assert!(matches!(c.get("file.txt"), Some(ChecksumElement::File(h)) if h == "deep_hash")); + } + + #[test] + fn from_hashmap_files_in_different_directories() { + let mut map = HashMap::new(); + map.insert("./dir1/file1.txt".to_string(), "hash1".to_string()); + map.insert("./dir2/file2.txt".to_string(), "hash2".to_string()); + let tree: ChecksumTree = map.into(); + + let root = match tree.as_ref().unwrap() { + ChecksumElement::Directory(d) => d, + _ => panic!(), + }; + let dot = match root.get(".").unwrap() { + ChecksumElement::Directory(d) => d, + _ => panic!(), + }; + assert_eq!(dot.len(), 2); + assert!(dot.contains_key("dir1")); + assert!(dot.contains_key("dir2")); + } + + #[test] + fn from_hashmap_roundtrip_through_reconciler() { + use crate::reconciler::Reconciler; + + let mut map = HashMap::new(); + map.insert("./dir/file.txt".to_string(), "hash1".to_string()); + map.insert("./other/nested/file.txt".to_string(), "hash2".to_string()); + let tree1: ChecksumTree = map.clone().into(); + let tree2: ChecksumTree = map.into(); + + let actions = Reconciler::reconcile(tree1, &tree2).unwrap(); + assert!( + actions.is_empty(), + "identical trees should produce zero actions" + ); + } + + #[test] + fn gzip_roundtrip() { + let mut map = HashMap::new(); + map.insert("./dir/file.txt".to_string(), "hash123".to_string()); + let tree: ChecksumTree = map.into(); + + let compressed = tree.to_gzip().unwrap(); + let restored = ChecksumTree::from_gzip(&compressed).unwrap(); + + assert_eq!( + serde_json::to_string(&tree).unwrap(), + serde_json::to_string(&restored).unwrap() + ); + } + + #[test] + fn gzip_roundtrip_empty_tree() { + let tree = ChecksumTree::default(); + + let compressed = tree.to_gzip().unwrap(); + let restored = ChecksumTree::from_gzip(&compressed).unwrap(); + + assert_eq!( + serde_json::to_string(&tree).unwrap(), + serde_json::to_string(&restored).unwrap() + ); + } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..7681f98 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,158 @@ +use clap::{ + builder::{styling::AnsiColor, Styles}, + Parser, +}; + +fn get_styles() -> Styles { + Styles::styled() + .header(AnsiColor::Green.on_default()) + .usage(AnsiColor::Green.on_default()) + .literal(AnsiColor::Green.on_default()) + .placeholder(AnsiColor::Green.on_default()) +} + +/// Fast sync with remote filesystem +#[derive(Parser, Debug, Clone)] +#[command(version, about, styles = get_styles())] +pub struct Args { + #[arg( + long, + help = "Name of the checksum file", + default_value = "./.syncbox.json.gz", + env = "SYNCBOX_CHECKSUM_FILE" + )] + pub checksum_file: String, + + #[arg( + long, + help = "Will skip execution and only creates the checksum file", + default_value_t = false + )] + pub checksum_only: bool, + + #[arg( + short, + long, + help = "Will upload checksum file every N files", + default_value_t = 0, + env = "SYNCBOX_INTERMITTENT_CHECKSUM_UPLOAD" + )] + pub intermittent_checksum_upload: usize, + + #[command(subcommand)] + pub transport: TransportType, + + #[arg( + long, + help = "Ignore corrupted checksum file and override", + default_value_t = false + )] + pub force: bool, + + #[arg( + short, + long, + help = "Concurrency limit for file operations", + default_value_t = 1, + env = "SYNCBOX_CONCURRENCY" + )] + pub concurrency: usize, + + #[arg( + long, + help = "Files of size below this threshold (in MBs) will be read and digested using SHA256, the others will use metadata as the checksum", + default_value_t = 100, + env = "SYNCBOX_FILE_THRESHOLD" + )] + pub file_size_threshold: u64, + + #[arg(short, long, default_value_t = false)] + pub skip_removal: bool, + + #[arg( + help = "Directory to diff against", + default_value = ".", + env = "SYNCBOX_DIRECTORY" + )] + pub directory: String, + + #[arg(long, help = "Skip first X actions", default_value_t = 0)] + pub skip: usize, + + #[arg( + long, + help = "Maximum number of retry attempts for failed operations", + default_value_t = 3, + env = "SYNCBOX_MAX_RETRIES" + )] + pub max_retries: usize, + + #[arg( + long, + help = "Initial retry delay in milliseconds", + default_value_t = 500, + env = "SYNCBOX_INITIAL_RETRY_DELAY" + )] + pub initial_retry_delay: u64, + + #[arg( + long, + help = "Maximum retry delay in seconds", + default_value_t = 30, + env = "SYNCBOX_MAX_RETRY_DELAY" + )] + pub max_retry_delay: u64, + + #[arg( + long, + help = "Enable automatic retry for transport operations", + default_value_t = true, + env = "SYNCBOX_ENABLE_RETRY_TRANSPORT" + )] + pub enable_retry_transport: bool, +} + +#[derive(Clone, Debug, Parser)] +pub enum TransportType { + Ftp { + #[arg(long, env = "FTP_HOST")] + ftp_host: String, + #[arg(long, env = "FTP_USER")] + ftp_user: String, + #[arg(long, env = "FTP_PASS")] + ftp_pass: String, + #[arg(long, default_value = ".", env = "FTP_DIR")] + ftp_dir: String, + #[arg(long, default_value_t = false, env = "FTP_USE_TLS")] + use_tls: bool, + }, + Sftp { + #[arg(long, env = "SFTP_HOST")] + host: String, + #[arg(long, env = "SFTP_USER")] + user: String, + #[arg(long, env = "SFTP_PASS")] + pass: String, + #[arg(long, default_value = ".", env = "SFTP_DIR")] + dir: String, + }, + Local { + #[arg(long, short)] + destination: String, + }, + S3 { + #[arg(long, env = "S3_BUCKET")] + bucket: String, + #[arg(long, env = "S3_REGION")] + region: String, + #[arg(long, env = "S3_ACCESS_KEY")] + access_key: String, + #[arg(long, env = "S3_SECRET_KEY")] + secret_key: String, + #[arg(long, default_value = "STANDARD", env = "S3_STORAGE_CLASS")] + storage_class: String, + #[arg(long, default_value = ".", env = "S3_DIRECTORY")] + directory: String, + }, + Dry, +} diff --git a/src/lib.rs b/src/lib.rs index 0e9c372..13c3903 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ pub mod checksum_tree; +pub mod config; pub mod progress; pub mod reconciler; +pub mod sync_engine; pub mod transport; +pub mod utils; diff --git a/src/main.rs b/src/main.rs index 2eab371..9753bbe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,156 +1,6 @@ -use clap::{ - builder::{styling::AnsiColor, Styles}, - Parser, -}; -use console::style; -use core::panic; -use futures::{future::try_join_all, stream, StreamExt}; -use indicatif::ProgressStyle; -use std::{ - collections::{HashMap, HashSet}, - error::Error, - ffi::OsString, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering::SeqCst}, - Arc, - }, - time::SystemTime, -}; -use syncbox::{ - checksum_tree::ChecksumTree, - progress, - reconciler::{Action, Reconciler}, - transport::{ - dry::DryTransport, ftp::Ftp, local::LocalFilesystem, s3::AwsS3, sftp::SFtp, Transport, - }, -}; -use tokio::{fs, sync::Mutex}; - -const PROGRESS_BAR_CHARS: &str = "โ–ฐโ–ฐโ–ฑ"; -const DEFAULT_FILE_SIZE_THRESHOLD: u64 = 1; - -fn get_styles() -> Styles { - Styles::styled() - .header(AnsiColor::Yellow.on_default()) - .usage(AnsiColor::Green.on_default()) - .literal(AnsiColor::Green.on_default()) - .placeholder(AnsiColor::Green.on_default()) -} - -/// Fast sync with remote filesystem -#[derive(Parser, Debug, Clone)] -#[command(version, about, styles = get_styles())] -struct Args { - #[arg( - long, - help = "Name of the checksum file", - default_value = "./.syncbox.json.gz", - env = "SYNCBOX_CHECKSUM_FILE" - )] - checksum_file: String, - - #[arg( - long, - help = "Will skip execution and only creates the checksum file", - default_value_t = false - )] - checksum_only: bool, - - #[arg( - short, - long, - help = "Will upload checksum file every N files", - default_value_t = 0, - env = "SYNCBOX_INTERMITTENT_CHECKSUM_UPLOAD" - )] - intermittent_checksum_upload: usize, - - #[command(subcommand)] - transport: TransportType, - - #[arg( - long, - help = "Ignore corrupted checksum file and override", - default_value_t = false - )] - force: bool, - - #[arg( - short, - long, - help = "Concurrency limit", - default_value_t = 1, - env = "SYNCBOX_CONCURRENCY" - )] - concurrency: usize, - - #[arg( - long, - help = "Files of size below this threshold (in MBs) will be read and digested using SHA256, the others will use metadata as the checksum", - default_value_t = DEFAULT_FILE_SIZE_THRESHOLD, - env = "SYNCBOX_FILE_THRESHOLD" - )] - file_size_threshold: u64, - - #[arg(short, long, default_value_t = false)] - skip_removal: bool, - - #[arg( - help = "Directory to diff against", - default_value = ".", - env = "SYNCBOX_DIRECTORY" - )] - directory: String, - - #[arg(long, help = "Skip first X actions", default_value_t = 0)] - skip: usize, -} - -#[derive(Clone, Debug, Parser)] -enum TransportType { - Ftp { - #[arg(long, env = "FTP_HOST")] - ftp_host: String, - #[arg(long, env = "FTP_USER")] - ftp_user: String, - #[arg(long, env = "FTP_PASS")] - ftp_pass: String, - #[arg(long, default_value = ".", env = "FTP_DIR")] - ftp_dir: String, - #[arg(long, default_value_t = false, env = "FTP_USE_TLS")] - use_tls: bool, - }, - Sftp { - #[arg(long, env = "SFTP_HOST")] - host: String, - #[arg(long, env = "SFTP_USER")] - user: String, - #[arg(long, env = "SFTP_PASS")] - pass: String, - #[arg(long, default_value = ".", env = "SFTP_DIR")] - dir: String, - }, - Local { - #[arg(long, short)] - destination: String, - }, - S3 { - #[arg(long, env = "S3_BUCKET")] - bucket: String, - #[arg(long, env = "S3_REGION")] - region: String, - #[arg(long, env = "S3_ACCESS_KEY")] - access_key: String, - #[arg(long, env = "S3_SECRET_KEY")] - secret_key: String, - #[arg(long, default_value = "STANDARD", env = "S3_STORAGE_CLASS")] - storage_class: String, - #[arg(long, default_value = ".", env = "S3_DIRECTORY")] - directory: String, - }, - Dry, -} +use clap::Parser; +use std::error::Error; +use syncbox::{config::Args, sync_engine::SyncEngine, utils::HumanBytes}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -158,472 +8,32 @@ async fn main() -> Result<(), Box> { dotenvy::dotenv().ok(); let args = Args::parse(); - let now = std::time::Instant::now(); std::env::set_current_dir(args.directory.clone())?; - println!("{} ๐Ÿ” Resolving files", style("[1/9]").dim().bold()); - - let mut ignored_files = vec![ - OsString::from(".git"), - OsString::from(".syncboxignore"), - OsString::from(".DS_Store"), - ]; - ignored_files.push((&args.checksum_file).into()); - let walker = ignore::WalkBuilder::new(".") - .hidden(false) - .filter_entry(move |entry| !ignored_files.contains(&entry.file_name().to_os_string())) - .add_custom_ignore_filename(".syncboxignore") - .build(); - let files = walker - .into_iter() - .collect::, _>>()? - .into_iter() - .filter(|entry| entry.file_type().map_or(false, |t| t.is_file())) - .map(|entry| entry.path().to_string_lossy().to_string()) - .collect::>(); - - // build map with checksums - println!("{} ๐Ÿงฌ Calculating checksums", style("[2/9]").dim().bold()); - let pb = &indicatif::ProgressBar::new(files.len().try_into()?); - pb.set_style( - ProgressStyle::with_template( - "[{elapsed_precise}] {bar:50.cyan/blue} {pos:>7}/{len:7} {wide_msg}", - ) - .unwrap() - .progress_chars(PROGRESS_BAR_CHARS), - ); - let next_checksum_tree: ChecksumTree = stream::iter(files) - .map(|filepath| { - let pb = pb.clone(); - tokio::spawn(async move { - pb.set_message(filepath.clone()); - let path_buf = PathBuf::from(filepath.clone()); - let metadata = tokio::fs::metadata(path_buf.as_path()).await.unwrap(); - let checksum = if metadata.len() > args.file_size_threshold * 1024 * 1024 { - format!( - "s{}_c{}_m{}", - metadata.len(), - metadata - .created()? - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs(), - metadata - .modified()? - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs() - ) - } else { - sha256::try_digest(path_buf.as_path()) - .map_err(|e| format!("Failed checksum of {filepath:?} with error {e:?}"))? - }; - pb.inc(1); - Ok((filepath, checksum)) as Result<_, Box> - }) - }) - .buffer_unordered(num_cpus::get()) - .collect::>() - .await - .into_iter() - .collect::, _>>()? - .into_iter() - .collect::, _>>()? - .into(); - pb.finish_and_clear(); - - if args.checksum_only { - println!("๐Ÿ’ฟ Writing checksum file to {}", args.checksum_file); - fs::write( - Path::new(&args.checksum_file), - next_checksum_tree.to_gzip()?, - ) - .await?; - return Ok(()); - } - - // get previous checksums using Transport - println!( - "{} ๐Ÿ“„ Fetching last checksum file", - style("[3/9]").dim().bold(), - ); - - let mut transport = make_transport(&args) - .await - .map_err(|e| format!("Connection failed with error: {e}"))?; - - let previous_checksum_tree = match transport - .read_last_checksum(Path::new(&args.checksum_file)) - .await - { - Ok(checksum) => checksum, - Err(e) => { - if args.force { - ChecksumTree::default() - } else { - panic!("{e}"); - } - } - }; - - // reconcile - println!("{} ๐Ÿšš Reconciling changes", style("[4/9]").dim().bold(),); - let todo = Arc::new(Reconciler::reconcile( - previous_checksum_tree, - &next_checksum_tree, - )?); - - if todo.is_empty() { - println!(" ๐Ÿคท Nothing to do"); - return Ok(()); - } - - println!( - "{} ๐Ÿš€ Executing {} action(s)", - style("[5/9]").dim().bold(), - style(todo.len()).bold() - ); - - let has_error = Arc::new(AtomicBool::new(false)); - - // first create directories - println!("{} ๐Ÿ“‚ Creating directories", style("[6/9]").dim().bold()); - let create_directory_actions: Vec<_> = todo - .iter() - .filter(|action| matches!(action, Action::Mkdir(_))) - .collect(); - for (i, action) in create_directory_actions.iter().enumerate() { - if i < args.skip { - continue; - } + // Create and run the sync engine + let sync_engine = SyncEngine::new(args); + let result = sync_engine.sync().await?; - let n = std::time::Instant::now(); - match action { - Action::Mkdir(path) => match transport.mkdir(path.as_path()).await { - Ok(_) => println!( - "โœ… Creating directory {}/{} {:?} in {:.2?}s", - i + 1, - create_directory_actions.len(), - path, - n.elapsed().as_secs_f64(), - ), - Err(error) => { - eprintln!( - "โŒ Error while creating directory {}/{} {:?}: {}", - i + 1, - create_directory_actions.len(), - path, - error - ); - has_error.store(true, SeqCst); - } - }, - _ => unreachable!(), - }; - } - - let checksum_path = Arc::new(PathBuf::from(&args.checksum_file)); - - // upload files - let bytes = Arc::new(AtomicU64::new(0)); - let progress_bars = Arc::new(indicatif::MultiProgress::new()); - let next_checksum_tree = Arc::new(Mutex::new(next_checksum_tree)); - let transports = Arc::new(Mutex::new( - try_join_all((0..args.concurrency).map(|_| make_transport(&args))).await?, - )); - let mut put_actions = todo - .iter() - .filter(|action| matches!(action, Action::Put(_))) - .cloned() - .collect::>(); - put_actions.sort_by(|a, b| { - let Action::Put(a) = a else { unreachable!() }; - let Action::Put(b) = b else { unreachable!() }; - if std::fs::metadata(a).unwrap().len() < std::fs::metadata(b).unwrap().len() { - std::cmp::Ordering::Less - } else { - std::cmp::Ordering::Greater - } - }); - let put_actions = Arc::new(put_actions); - let total_to_upload = Arc::new(AtomicU64::new( - put_actions - .iter() - .map(|action| { - let Action::Put(path) = action else { - unreachable!(); - }; - std::fs::metadata(path).unwrap().len() - }) - .sum::(), - )); println!( - "{} ๐Ÿ‚ Uploading {} files ({})", - style("[7/9]").dim().bold(), - put_actions.len(), - total_to_upload.to_human_size() + "โœจ Done. Transferred {} files ({}) in {:.2?}s", + result.files_uploaded, + result.bytes_transferred.to_human_size(), + result.duration.as_secs_f64() ); - let put_actions_len = put_actions.len(); - let finished_paths = Arc::new(Mutex::new(HashSet::new())); - let put_actions = put_actions.iter() - .enumerate() - .skip((args.skip as i64 - create_directory_actions.len() as i64).max(0) as usize) - .map(|(i, action)| { - let total_to_upload = Arc::clone(&total_to_upload); - let checksum_path = Arc::clone(&checksum_path); - let todo = Arc::clone(&todo); - let finished_paths = Arc::clone(&finished_paths); - let transports = Arc::clone(&transports); - let progress_bars = Arc::clone(&progress_bars); - let bytes = Arc::clone(&bytes); - let next_checksum_tree = Arc::clone(&next_checksum_tree); - let has_error = Arc::clone(&has_error); - let action = action.clone(); - tokio::spawn(async move { - let Action::Put(path) = action else { - unreachable!(); - }; - - let file = fs::File::open(&path).await.unwrap(); - let metadata = file.metadata().await.unwrap(); - let mut transport = transports.lock().await.pop().unwrap(); - let pb = indicatif::ProgressBar::new(metadata.len()); - let pb = Arc::new(progress_bars.add(pb)); - let mut template = format!("[{}/{}] ", i + 1, put_actions_len); - template.push_str("[{elapsed_precise}] {wide_bar:.cyan/blue} {bytes}/{total_bytes} [{bytes_per_sec}] {msg}"); - pb.set_style( - ProgressStyle::with_template(&template) - .unwrap() - .progress_chars(PROGRESS_BAR_CHARS), - ); - let msg = path.to_path_buf().to_str().unwrap().to_string(); - pb.set_message(msg); - pb.inc(0); - let pb_inner = Arc::clone(&pb); - let file = progress::ProgressStream::new(file,Box::new(move |uploaded| { - pb_inner.set_position(uploaded); - })); - match transport - .write( - path.as_path(), - Box::new(file), - metadata.len() - ) - .await - { - Ok(b) => { - bytes.fetch_add(b, SeqCst); - finished_paths.lock().await.insert(path.clone()); - let message = format!("{} | {} remaining", - path.to_string_lossy(), - (total_to_upload.load(SeqCst) - bytes.load(SeqCst)).to_human_size(), - ); - pb.finish_with_message(message.clone()); - - // if we are running on the CI, print successful message - if std::env::var("CI").is_ok() { - println!("โœ… {}", message); - } - - // if we are uploading checksums intermittently, do it now - if args.intermittent_checksum_upload > 0 - && finished_paths.lock().await.len() > 0 && finished_paths.lock().await.len() - % args.intermittent_checksum_upload - == 0 - { - let mut intermittent_checksum = next_checksum_tree.lock().await.clone(); - let finished_paths = finished_paths.lock().await; - todo.iter().filter_map(|action| { - let path = match action { - Action::Put(path) => path, - Action::Remove(path) => path, - Action::Mkdir(_) => return None, // done already above - }; - if !finished_paths.contains(path) { - Some(path) - } else { - None - } - }).for_each(|path| { - intermittent_checksum.remove_at(path); - }); - pb.set_message("๐Ÿ“ธ Uploading intermittent checksum"); - if let Err(e) = transport.write_last_checksum(checksum_path.as_path(), &intermittent_checksum).await { - pb.set_message(format!("โŒ Error while uploading intermittent checksum: {}", e)); - } else { - pb.set_message(message); - } - } - } - Err(error) => { - let message = format!("โŒ Error while copying {:?}: {}", path, error); - pb.abandon_with_message(message.clone()); - next_checksum_tree.lock().await.remove_at(path.as_path()); - has_error.store(true, SeqCst); - - // if we are running on the CI, print error message - if std::env::var("CI").is_ok() { - println!("{message}"); - } - } - }; - transports.lock().await.push(transport); - }) - }); - - stream::iter(put_actions) - .buffer_unordered(args.concurrency) - .collect::>() - .await - .into_iter() - .collect::, _>>()?; - - // removing files - if args.skip_removal { - println!( - "{} ๐Ÿงป Removing files (skipping)", - style("[8/9]").dim().bold() - ); - } else { - println!("{} ๐Ÿงป Removing files", style("[8/9]").dim().bold()); - let remove_actions: Vec<_> = todo - .iter() - .filter(|action| matches!(action, Action::Remove(_))) - .cloned() - .collect(); - let remove_actions_len = remove_actions.len(); - let remove_actions = remove_actions - .iter() - .enumerate() - .skip( - (args.skip as i64 - create_directory_actions.len() as i64 - put_actions_len as i64) - .max(0) as usize, - ) - .map(|(i, action)| { - let transports = Arc::clone(&transports); - let has_error = Arc::clone(&has_error); - let action = action.clone(); - tokio::spawn(async move { - let mut transport = transports.lock().await.pop().unwrap(); - - let n = std::time::Instant::now(); - - match action { - Action::Remove(path) => { - match transport.remove(path.as_path()).await { - Ok(_) => { - println!( - "โœ… Removed {}/{} file: {:?} in {:.2?}s", - i + 1, - remove_actions_len, - path, - n.elapsed().as_secs_f64(), - ); - } - Err(error) => { - eprintln!("โŒ Error while removing {:?}: {}", path, error); - has_error.store(true, SeqCst); - } - }; - } - _ => unreachable!(), - }; - transports.lock().await.push(transport); - }) - }); - - stream::iter(remove_actions) - .buffer_unordered(args.concurrency) - .collect::>() - .await - .into_iter() - .collect::, _>>()?; - } - - let mut transport = make_transport(&args).await?; - - println!("{} ๐Ÿ Uploading checksum", style("[9/9]").dim().bold()); - transport - .write_last_checksum(checksum_path.as_path(), &*next_checksum_tree.lock().await) - .await?; - - transport.close().await?; + println!("๐Ÿ“Š Summary:"); + println!(" Files uploaded: {}", result.files_uploaded); + println!(" Files removed: {}", result.files_removed); + println!(" Directories created: {}", result.directories_created); println!( - "โœจ Done. Transfered {} in {:.2?}s", - bytes.to_human_size(), - now.elapsed().as_secs_f64() + " Data transferred: {}", + result.bytes_transferred.to_human_size() ); - if has_error.load(SeqCst) { - panic!("There were errors"); + if result.had_errors { + return Err("Some operations failed during sync".into()); } Ok(()) } - -async fn make_transport( - args: &Args, -) -> Result, Box> { - Ok(match &args.transport { - TransportType::Ftp { - ftp_host, - ftp_user, - ftp_pass, - ftp_dir, - use_tls, - } => Box::new( - Ftp::new(ftp_host, ftp_user, ftp_pass, ftp_dir) - .connect(*use_tls) - .await?, - ), - TransportType::Sftp { - host, - user, - pass, - dir, - } => Box::new(SFtp::new(host, user, pass, dir).await?), - TransportType::Local { destination } => Box::new(LocalFilesystem::new(destination)), - TransportType::S3 { - bucket, - region, - access_key, - secret_key, - storage_class, - directory, - } => Box::new(AwsS3::new( - bucket, - region, - access_key, - secret_key, - storage_class, - directory.into(), - )?), - TransportType::Dry => Box::new(DryTransport), - }) -} - -trait HumanBytes { - fn to_human_size(self) -> String; -} - -impl HumanBytes for u64 { - fn to_human_size(self) -> String { - let value = self; - if value > 1024 * 1024 * 1024 { - format!("{:.2?}GB", value as f64 / 1024.0 / 1024.0 / 1024.0) - } else if value > 1024 * 1024 { - format!("{:.2?}MB", value as f64 / 1024.0 / 1024.0) - } else if value > 1024 { - format!("{:.2?}KB", value as f64 / 1024.0) - } else { - format!("{}B", value) - } - } -} - -impl HumanBytes for &AtomicU64 { - fn to_human_size(self) -> String { - let value = self.load(SeqCst); - value.to_human_size() - } -} diff --git a/src/reconciler.rs b/src/reconciler.rs index e6568c0..188dcce 100644 --- a/src/reconciler.rs +++ b/src/reconciler.rs @@ -339,4 +339,46 @@ mod tests { ) ); } + + #[test] + fn unchanged_file_produces_no_actions() { + let mut prev = HashMap::new(); + prev.insert("./file.txt".to_string(), "same_hash".to_string()); + let prev: ChecksumTree = prev.into(); + let mut next = HashMap::new(); + next.insert("./file.txt".to_string(), "same_hash".to_string()); + let next: ChecksumTree = next.into(); + + let diff = Reconciler::reconcile(prev, &next).unwrap(); + assert!(diff.is_empty()); + } + + #[test] + fn multiple_files_in_sibling_directories() { + let mut prev = HashMap::new(); + prev.insert("./alpha/file1.txt".to_string(), "hash1".to_string()); + prev.insert("./beta/file2.txt".to_string(), "hash2".to_string()); + let prev: ChecksumTree = prev.into(); + + let mut next = HashMap::new(); + next.insert("./alpha/file1.txt".to_string(), "hash1_changed".to_string()); + next.insert("./beta/file2.txt".to_string(), "hash2".to_string()); + next.insert("./gamma/file3.txt".to_string(), "hash3".to_string()); + let next: ChecksumTree = next.into(); + + let diff = Reconciler::reconcile(prev, &next).unwrap(); + + assert!(diff.contains(&Action::Put("./alpha/file1.txt".into()))); + assert!(diff.contains(&Action::Mkdir("./gamma".into()))); + assert!(diff.contains(&Action::Put("./gamma/file3.txt".into()))); + // beta/file2.txt unchanged, should not appear + assert!(!diff + .iter() + .any(|a| matches!(a, Action::Put(p) if p == &PathBuf::from("./beta/file2.txt")))); + } + + #[test] + fn version_same_ok() { + assert_eq!(check_version("0.5.4", "0.5.4").ok(), Some(())); + } } diff --git a/src/sync_engine.rs b/src/sync_engine.rs new file mode 100644 index 0000000..f9af72d --- /dev/null +++ b/src/sync_engine.rs @@ -0,0 +1,732 @@ +use crate::checksum_tree::ChecksumTree; +use crate::config::Args; +use crate::progress; +use crate::reconciler::{Action, Reconciler}; +use crate::transport::{ + retry::{ConfigBasedTransportFactory, RetryConfig, RetryTransport, TransportFactory}, + Transport, +}; +use crate::utils::HumanBytes; +use console::style; +use futures::{stream, StreamExt}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use std::{ + collections::{HashMap, HashSet}, + error::Error, + ffi::OsString, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, AtomicU64, Ordering::SeqCst}, + Arc, + }, + time::{Duration, SystemTime}, +}; +use tokio::{fs, sync::Mutex}; + +const PROGRESS_BAR_CHARS: &str = "โ–ฐโ–ฐโ–ฑ"; + +#[derive(Debug)] +pub struct SyncResult { + pub files_uploaded: usize, + pub bytes_transferred: u64, + pub files_removed: usize, + pub directories_created: usize, + pub duration: std::time::Duration, + pub had_errors: bool, +} + +pub struct SyncEngine { + args: Args, +} + +impl SyncEngine { + pub fn new(args: Args) -> Self { + Self { args } + } + + pub async fn sync(&self) -> Result> { + let start_time = std::time::Instant::now(); + + // Phase 1: Discover files and calculate checksums + let next_checksum_tree = self.discover_and_calculate_checksums().await?; + + if self.args.checksum_only { + return self + .save_checksum_only(&next_checksum_tree, start_time) + .await; + } + + // Phase 2: Fetch previous checksums + let previous_checksum_tree = self.fetch_previous_checksums().await?; + + // Phase 3: Reconcile changes + let actions = self.reconcile_changes(previous_checksum_tree, &next_checksum_tree)?; + + if actions.is_empty() { + println!(" ๐Ÿคท Nothing to do"); + return Ok(SyncResult { + files_uploaded: 0, + bytes_transferred: 0, + files_removed: 0, + directories_created: 0, + duration: start_time.elapsed(), + had_errors: false, + }); + } + + // Phase 4: Execute operations + let sync_stats = self + .execute_sync_operations(actions, next_checksum_tree) + .await?; + + Ok(SyncResult { + files_uploaded: sync_stats.files_uploaded, + bytes_transferred: sync_stats.bytes_transferred, + files_removed: sync_stats.files_removed, + directories_created: sync_stats.directories_created, + duration: start_time.elapsed(), + had_errors: sync_stats.had_errors, + }) + } + + async fn discover_and_calculate_checksums( + &self, + ) -> Result> { + println!("{} ๐Ÿ” Resolving files", style("[1/9]").dim().bold()); + + let mut ignored_files = vec![ + OsString::from(".git"), + OsString::from(".syncboxignore"), + OsString::from(".DS_Store"), + ]; + ignored_files.push((&self.args.checksum_file).into()); + + let walker = ignore::WalkBuilder::new(".") + .hidden(false) + .filter_entry(move |entry| !ignored_files.contains(&entry.file_name().to_os_string())) + .add_custom_ignore_filename(".syncboxignore") + .build(); + + let files = walker + .into_iter() + .collect::, _>>()? + .into_iter() + .filter(|entry| entry.file_type().is_some_and(|t| t.is_file())) + .map(|entry| entry.path().to_string_lossy().to_string()) + .collect::>(); + + println!("{} ๐Ÿงฌ Calculating checksums", style("[2/9]").dim().bold()); + let pb = ProgressBar::new(files.len().try_into()?); + pb.set_style( + ProgressStyle::with_template( + "[{elapsed_precise}] {bar:50.cyan/blue} {pos:>7}/{len:7} {wide_msg}", + ) + .unwrap() + .progress_chars(PROGRESS_BAR_CHARS), + ); + + let checksum_tree: ChecksumTree = stream::iter(files) + .map(|filepath| { + let pb = pb.clone(); + let file_size_threshold = self.args.file_size_threshold; + tokio::spawn(async move { + pb.set_message(filepath.clone()); + let result = + Self::calculate_file_checksum(&filepath, file_size_threshold).await; + pb.inc(1); + result + }) + }) + .buffer_unordered(num_cpus::get()) + .collect::>() + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .collect::, _>>()? + .into(); + + pb.finish_and_clear(); + Ok(checksum_tree) + } + + async fn calculate_file_checksum( + filepath: &str, + file_size_threshold: u64, + ) -> Result<(String, String), Box> { + let path_buf = PathBuf::from(filepath); + let metadata = tokio::fs::metadata(&path_buf).await?; + + let checksum = if metadata.len() > file_size_threshold * 1024 * 1024 { + format!( + "s{}_c{}_m{}", + metadata.len(), + metadata + .created()? + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs(), + metadata + .modified()? + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs() + ) + } else { + sha256::try_digest(&path_buf) + .map_err(|e| format!("Failed checksum of {filepath:?} with error {e:?}"))? + }; + + Ok((filepath.to_string(), checksum)) + } + + async fn save_checksum_only( + &self, + checksum_tree: &ChecksumTree, + start_time: std::time::Instant, + ) -> Result> { + println!("๐Ÿ’ฟ Writing checksum file to {}", self.args.checksum_file); + fs::write( + Path::new(&self.args.checksum_file), + checksum_tree.to_gzip()?, + ) + .await?; + + Ok(SyncResult { + files_uploaded: 0, + bytes_transferred: 0, + files_removed: 0, + directories_created: 0, + duration: start_time.elapsed(), + had_errors: false, + }) + } + + async fn fetch_previous_checksums( + &self, + ) -> Result> { + println!( + "{} ๐Ÿ“„ Fetching last checksum file", + style("[3/9]").dim().bold(), + ); + + let mut transport = self.create_transport().await?; + + match transport + .read_last_checksum(Path::new(&self.args.checksum_file)) + .await + { + Ok(checksum) => Ok(checksum), + Err(e) => { + if self.args.force { + Ok(ChecksumTree::default()) + } else { + Err(format!("Failed to fetch previous checksums: {e}").into()) + } + } + } + } + + fn reconcile_changes( + &self, + previous: ChecksumTree, + current: &ChecksumTree, + ) -> Result, Box> { + println!("{} ๐Ÿšš Reconciling changes", style("[4/9]").dim().bold()); + Reconciler::reconcile(previous, current) + } + + async fn execute_sync_operations( + &self, + actions: Vec, + final_checksum_tree: ChecksumTree, + ) -> Result> { + println!( + "{} ๐Ÿš€ Executing {} action(s)", + style("[5/9]").dim().bold(), + style(actions.len()).bold() + ); + + let mut stats = SyncStats::default(); + let has_error = Arc::new(AtomicBool::new(false)); + + // Phase 4a: Create directories + stats.directories_created = self + .execute_directory_operations(&actions, &has_error) + .await?; + + // Phase 4b: Upload files + let upload_stats = self + .execute_file_uploads(&actions, &final_checksum_tree, &has_error) + .await?; + stats.files_uploaded = upload_stats.files_uploaded; + stats.bytes_transferred = upload_stats.bytes_transferred; + + // Phase 4c: Remove files + if !self.args.skip_removal { + stats.files_removed = self.execute_file_removals(&actions, &has_error).await?; + } else { + println!( + "{} ๐Ÿงป Removing files (skipping)", + style("[8/9]").dim().bold() + ); + } + + // Phase 4d: Upload final checksum + self.upload_final_checksum(&final_checksum_tree).await?; + + stats.had_errors = has_error.load(SeqCst); + if stats.had_errors { + return Err("Some operations failed during sync".into()); + } + + Ok(stats) + } + + async fn execute_directory_operations( + &self, + actions: &[Action], + has_error: &Arc, + ) -> Result> { + println!("{} ๐Ÿ“‚ Creating directories", style("[6/9]").dim().bold()); + + let create_actions: Vec<_> = actions + .iter() + .filter(|action| matches!(action, Action::Mkdir(_))) + .collect(); + + let mut transport = self.create_transport().await?; + + for (i, action) in create_actions.iter().enumerate() { + if i < self.args.skip { + continue; + } + + let start_time = std::time::Instant::now(); + match action { + Action::Mkdir(path) => match transport.mkdir(path.as_path()).await { + Ok(_) => println!( + "โœ… Creating directory {}/{} {:?} in {:.2?}s", + i + 1, + create_actions.len(), + path, + start_time.elapsed().as_secs_f64(), + ), + Err(error) => { + eprintln!( + "โŒ Error while creating directory {}/{} {:?}: {}", + i + 1, + create_actions.len(), + path, + error + ); + has_error.store(true, SeqCst); + } + }, + _ => unreachable!(), + } + } + + Ok(create_actions.len()) + } + + async fn execute_file_uploads( + &self, + actions: &[Action], + checksum_tree: &ChecksumTree, + has_error: &Arc, + ) -> Result> { + let put_actions: Vec<_> = actions + .iter() + .filter(|action| matches!(action, Action::Put(_))) + .cloned() + .collect(); + + if put_actions.is_empty() { + return Ok(UploadStats::default()); + } + + // Sort by file size (smallest first for better progress) + let mut sorted_actions = put_actions.clone(); + sorted_actions.sort_by(|a, b| { + let Action::Put(a) = a else { unreachable!() }; + let Action::Put(b) = b else { unreachable!() }; + std::fs::metadata(a) + .unwrap() + .len() + .cmp(&std::fs::metadata(b).unwrap().len()) + }); + + let total_bytes = Arc::new(AtomicU64::new( + sorted_actions + .iter() + .map(|action| { + let Action::Put(path) = action else { + unreachable!() + }; + std::fs::metadata(path).unwrap().len() + }) + .sum::(), + )); + + println!( + "{} ๐Ÿ‚ Uploading {} files ({})", + style("[7/9]").dim().bold(), + sorted_actions.len(), + total_bytes.load(SeqCst).to_human_size() + ); + + let uploaded_bytes = Arc::new(AtomicU64::new(0)); + let progress_bars = Arc::new(MultiProgress::new()); + let checksum_path = Arc::new(PathBuf::from(&self.args.checksum_file)); + let finished_paths = Arc::new(Mutex::new(HashSet::new())); + + // Create transport pool + let transport_pool = Arc::new(Mutex::new(self.create_transport_pool().await?)); + + let sorted_actions_len = sorted_actions.len(); + let upload_tasks = sorted_actions + .into_iter() + .enumerate() + .skip(self.args.skip) + .map(|(i, action)| { + let action = action.clone(); + let total_bytes = Arc::clone(&total_bytes); + let uploaded_bytes = Arc::clone(&uploaded_bytes); + let progress_bars = Arc::clone(&progress_bars); + let transport_pool = Arc::clone(&transport_pool); + let has_error = Arc::clone(has_error); + let checksum_file = self.args.checksum_file.clone(); + let checksum_path = Arc::clone(&checksum_path); + let finished_paths = Arc::clone(&finished_paths); + let intermittent_upload_interval = self.args.intermittent_checksum_upload; + let checksum_tree = checksum_tree.clone(); + + tokio::spawn(async move { + Self::upload_single_file( + action, + i, + sorted_actions_len, + total_bytes, + uploaded_bytes, + progress_bars, + transport_pool, + has_error, + checksum_file, + checksum_path, + finished_paths, + intermittent_upload_interval, + checksum_tree, + ) + .await + }) + }); + + let results = stream::iter(upload_tasks) + .buffer_unordered(self.args.concurrency) + .collect::>() + .await; + + let mut upload_stats = UploadStats::default(); + for result in results { + match result? { + Ok(bytes) => { + upload_stats.files_uploaded += 1; + upload_stats.bytes_transferred += bytes; + } + Err(_) => { + // Error already logged in upload_single_file + } + } + } + + Ok(upload_stats) + } + + #[allow(clippy::too_many_arguments)] + async fn upload_single_file( + action: Action, + index: usize, + total_files: usize, + total_bytes: Arc, + uploaded_bytes: Arc, + progress_bars: Arc, + transport_pool: Arc>>>, + has_error: Arc, + _checksum_file: String, + checksum_path: Arc, + finished_paths: Arc>>, + intermittent_upload_interval: usize, + checksum_tree: ChecksumTree, + ) -> Result> { + let Action::Put(path) = action else { + unreachable!() + }; + + let metadata = fs::metadata(&path).await?; + let file_size = metadata.len(); + + // Get transport from pool + let mut transport = { + let mut pool = transport_pool.lock().await; + pool.pop().ok_or("No transport available")? + }; + + // Setup progress bar + let pb = ProgressBar::new(file_size); + let pb = Arc::new(progress_bars.add(pb)); + + let mut template = format!("[{}/{}] ", index + 1, total_files); + template.push_str("[{elapsed_precise}] {wide_bar:.cyan/blue} {bytes}/{total_bytes} [{bytes_per_sec}] {msg}"); + + pb.set_style( + ProgressStyle::with_template(&template) + .unwrap() + .progress_chars(PROGRESS_BAR_CHARS), + ); + + pb.set_message(path.to_string_lossy().to_string()); + + // Upload the file with retries (re-open file on each attempt) + const MAX_UPLOAD_RETRIES: usize = 3; + let mut last_error = None; + + for attempt in 0..=MAX_UPLOAD_RETRIES { + if attempt > 0 { + let wait = Duration::from_millis(500 * 2u64.pow(attempt as u32 - 1)); + eprintln!( + "โš ๏ธ Retrying upload for {:?} (attempt {}/{}) in {:.1}s...", + path, + attempt + 1, + MAX_UPLOAD_RETRIES + 1, + wait.as_secs_f64() + ); + // Return transport to pool before sleeping so others can use it + transport_pool.lock().await.push(transport); + tokio::time::sleep(wait).await; + transport = { + let mut pool = transport_pool.lock().await; + pool.pop().ok_or("No transport available")? + }; + pb.set_position(0); + } + + // Open file and create progress stream fresh for each attempt + let file = fs::File::open(&path).await?; + let pb_inner = Arc::clone(&pb); + let progress_file = progress::ProgressStream::new( + file, + Box::new(move |uploaded| { + pb_inner.set_position(uploaded); + }), + ); + + match transport + .write(path.as_path(), Box::new(progress_file), file_size) + .await + { + Ok(bytes_written) => { + uploaded_bytes.fetch_add(bytes_written, SeqCst); + finished_paths.lock().await.insert(path.clone()); + + let remaining = total_bytes.load(SeqCst) - uploaded_bytes.load(SeqCst); + let message = format!( + "{} | {} remaining", + path.to_string_lossy(), + remaining.to_human_size() + ); + + pb.finish_with_message(message.clone()); + + // Print success message in CI + if std::env::var("CI").is_ok() { + println!("โœ… {}", message); + } + + // Handle intermittent checksum upload + let finished_count = finished_paths.lock().await.len(); + if intermittent_upload_interval > 0 + && finished_count > 0 + && finished_count % intermittent_upload_interval == 0 + { + pb.set_message("๐Ÿ“ธ Uploading intermittent checksum"); + if let Err(e) = transport + .write_last_checksum(checksum_path.as_path(), &checksum_tree) + .await + { + pb.set_message(format!( + "โŒ Error uploading intermittent checksum: {}", + e + )); + } else { + pb.set_message(message); + } + } + + // Return transport to pool + transport_pool.lock().await.push(transport); + return Ok(bytes_written); + } + Err(error) => { + eprintln!("โŒ Upload failed for {:?}: {}", path, error); + last_error = Some(error); + } + } + } + + let error = last_error.unwrap(); + let message = format!("โŒ Error while uploading {:?}: {}", path, error); + pb.abandon_with_message(message.clone()); + has_error.store(true, SeqCst); + + if std::env::var("CI").is_ok() { + println!("{}", message); + } + + // Return transport to pool even on error + transport_pool.lock().await.push(transport); + Err(error) + } + + async fn execute_file_removals( + &self, + actions: &[Action], + has_error: &Arc, + ) -> Result> { + println!("{} ๐Ÿงป Removing files", style("[8/9]").dim().bold()); + + let remove_actions: Vec<_> = actions + .iter() + .filter(|action| matches!(action, Action::Remove(_))) + .cloned() + .collect(); + + let transport_pool = Arc::new(Mutex::new(self.create_transport_pool().await?)); + + let removal_tasks = + remove_actions + .iter() + .enumerate() + .skip(self.args.skip) + .map(|(i, action)| { + let action = action.clone(); + let transport_pool = Arc::clone(&transport_pool); + let has_error = Arc::clone(has_error); + let total_removals = remove_actions.len(); + + tokio::spawn(async move { + let mut transport = { + let mut pool = transport_pool.lock().await; + pool.pop().ok_or("No transport available")? + }; + + let start_time = std::time::Instant::now(); + let result = match action { + Action::Remove(path) => match transport.remove(path.as_path()).await { + Ok(_) => { + println!( + "โœ… Removed {}/{} file: {:?} in {:.2?}s", + i + 1, + total_removals, + path, + start_time.elapsed().as_secs_f64(), + ); + Ok(()) + } + Err(error) => { + eprintln!("โŒ Error while removing {:?}: {}", path, error); + has_error.store(true, SeqCst); + Err(error) + } + }, + _ => unreachable!(), + }; + + // Return transport to pool + transport_pool.lock().await.push(transport); + result + }) + }); + + let results = stream::iter(removal_tasks) + .buffer_unordered(self.args.concurrency) + .collect::>() + .await; + + let mut removed_count = 0; + for result in results { + match result? { + Ok(_) => removed_count += 1, + Err(_) => { + // Error already logged + } + } + } + + Ok(removed_count) + } + + async fn upload_final_checksum( + &self, + checksum_tree: &ChecksumTree, + ) -> Result<(), Box> { + println!("{} ๐Ÿ Uploading checksum", style("[9/9]").dim().bold()); + + let mut transport = self.create_transport().await?; + transport + .write_last_checksum(Path::new(&self.args.checksum_file), checksum_tree) + .await?; + transport.close().await?; + + Ok(()) + } + + async fn create_transport( + &self, + ) -> Result, Box> { + if self.args.enable_retry_transport { + self.create_single_retry_transport().await + } else { + self.create_single_base_transport().await + } + } + + async fn create_single_retry_transport( + &self, + ) -> Result, Box> { + let factory = Arc::new(ConfigBasedTransportFactory::new(self.args.clone())); + let retry_config = RetryConfig::from_args(&self.args); + let retry_transport = RetryTransport::new(factory, retry_config); + Ok(Box::new(retry_transport) as Box) + } + + async fn create_single_base_transport( + &self, + ) -> Result, Box> { + let factory = ConfigBasedTransportFactory::new(self.args.clone()); + factory.create().await + } + + async fn create_transport_pool( + &self, + ) -> Result>, Box> { + let mut transports = Vec::new(); + for _ in 0..self.args.concurrency { + transports.push(self.create_transport().await?); + } + Ok(transports) + } +} + +#[derive(Debug, Default)] +struct SyncStats { + files_uploaded: usize, + bytes_transferred: u64, + files_removed: usize, + directories_created: usize, + had_errors: bool, +} + +#[derive(Debug, Default)] +struct UploadStats { + files_uploaded: usize, + bytes_transferred: u64, +} diff --git a/src/transport.rs b/src/transport.rs index c8460a7..f1f98ed 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -5,6 +5,7 @@ use tokio::io::AsyncRead; pub mod dry; pub mod ftp; pub mod local; +pub mod retry; pub mod s3; pub mod sftp; diff --git a/src/transport/dry.rs b/src/transport/dry.rs index 5eb7ff2..c14ac68 100644 --- a/src/transport/dry.rs +++ b/src/transport/dry.rs @@ -59,3 +59,30 @@ impl Transport for DryTransport { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn all_operations_succeed() { + let mut transport = DryTransport; + + let read_result = transport.read(Path::new("file.txt")).await; + assert!(read_result.is_ok()); + + let mkdir_result = transport.mkdir(Path::new("dir")).await; + assert!(mkdir_result.is_ok()); + + let remove_result = transport.remove(Path::new("file.txt")).await; + assert!(remove_result.is_ok()); + + let reader: Box = Box::new(Cursor::new(vec![1, 2, 3])); + let write_result = transport.write(Path::new("file.txt"), reader, 3).await; + assert!(write_result.is_ok()); + assert_eq!(write_result.unwrap(), 3); + + let close_result = Box::new(DryTransport).close().await; + assert!(close_result.is_ok()); + } +} diff --git a/src/transport/local.rs b/src/transport/local.rs index cac9c83..7203b8e 100644 --- a/src/transport/local.rs +++ b/src/transport/local.rs @@ -55,10 +55,72 @@ impl Transport for LocalFilesystem { &mut self, pathname: &Path, ) -> Result<(), Box> { - Ok(tokio::fs::remove_file(pathname).await?) + let mut path = self.dir.clone(); + path.push(pathname); + Ok(tokio::fs::remove_file(path).await?) } async fn close(self: Box) -> Result<(), Box> { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use tokio::io::AsyncRead; + + #[tokio::test] + async fn write_and_read_file() { + let tmp = TempDir::new().unwrap(); + let mut transport = LocalFilesystem::new(tmp.path()); + + let data = b"hello world"; + let reader: Box = + Box::new(std::io::Cursor::new(data.to_vec())); + transport + .write(Path::new("test.txt"), reader, data.len() as u64) + .await + .unwrap(); + + let result = transport.read(Path::new("test.txt")).await.unwrap(); + assert_eq!(result, data); + } + + #[tokio::test] + async fn mkdir_creates_directory() { + let tmp = TempDir::new().unwrap(); + let mut transport = LocalFilesystem::new(tmp.path()); + + transport.mkdir(Path::new("subdir")).await.unwrap(); + assert!(tmp.path().join("subdir").is_dir()); + } + + #[tokio::test] + async fn remove_deletes_file() { + let tmp = TempDir::new().unwrap(); + let mut transport = LocalFilesystem::new(tmp.path()); + + // Write a file first + let data = b"to be removed"; + let reader: Box = + Box::new(std::io::Cursor::new(data.to_vec())); + transport + .write(Path::new("removeme.txt"), reader, data.len() as u64) + .await + .unwrap(); + + transport.remove(Path::new("removeme.txt")).await.unwrap(); + assert!(!tmp.path().join("removeme.txt").exists()); + } + + #[tokio::test] + async fn read_nonexistent_file_returns_error() { + let tmp = TempDir::new().unwrap(); + let mut transport = LocalFilesystem::new(tmp.path()); + + let result = transport.read(Path::new("does_not_exist.txt")).await; + assert!(result.is_err()); + } +} diff --git a/src/transport/retry.rs b/src/transport/retry.rs new file mode 100644 index 0000000..08e6c48 --- /dev/null +++ b/src/transport/retry.rs @@ -0,0 +1,527 @@ +//! Retry transport wrapper that provides automatic retry functionality for transport operations. +//! +//! This module provides a `RetryTransport` struct that wraps any transport implementation +//! and automatically retries failed operations using exponential backoff. When an operation +//! fails, the transport connection is dropped and a new one is created for the next attempt. + +use super::Transport; +use crate::checksum_tree::ChecksumTree; +use crate::config::Args; +use rand::Rng; +use std::{error::Error, io::Cursor, path::Path, sync::Arc, time::Duration}; +use tokio::{io::AsyncRead, time::sleep}; + +/// A factory trait for creating transport instances +#[async_trait::async_trait] +pub trait TransportFactory: Send + Sync { + async fn create( + &self, + ) -> Result, Box>; +} + +/// Configuration for retry behavior +#[derive(Clone, Debug)] +pub struct RetryConfig { + pub max_retries: usize, + pub initial_delay: Duration, + pub max_delay: Duration, +} + +impl RetryConfig { + pub fn new(max_retries: usize, initial_delay_ms: u64, max_delay_secs: u64) -> Self { + Self { + max_retries, + initial_delay: Duration::from_millis(initial_delay_ms), + max_delay: Duration::from_secs(max_delay_secs), + } + } + + pub fn from_args(args: &Args) -> Self { + Self::new( + args.max_retries, + args.initial_retry_delay, + args.max_retry_delay, + ) + } +} + +fn delay_with_jitter(delay: Duration) -> Duration { + let jitter_ms = delay.as_millis() / 4; + if jitter_ms == 0 { + return delay; + } + let jitter = rand::thread_rng().gen_range(0..=jitter_ms) as u64; + delay + Duration::from_millis(jitter) +} + +/// Macro to deduplicate retry loop logic for retryable operations. +macro_rules! retry_op { + ($self:ident, |$transport:ident| $op:expr) => {{ + let mut last_error = None; + let mut delay = $self.config.initial_delay; + + for attempt in 0..=$self.config.max_retries { + if $self.transport.is_none() { + match $self.factory.create().await { + Ok(transport) => $self.transport = Some(transport), + Err(e) => { + eprintln!( + "โš ๏ธ Transport creation failed (attempt {}/{}): {}", + attempt + 1, + $self.config.max_retries + 1, + e + ); + last_error = Some(e); + if attempt < $self.config.max_retries { + let wait = delay_with_jitter(delay); + eprintln!(" Retrying in {:.1}s...", wait.as_secs_f64()); + sleep(wait).await; + delay = std::cmp::min(delay * 2, $self.config.max_delay); + } + continue; + } + } + } + + if let Some($transport) = &mut $self.transport { + match $op.await { + Ok(result) => return Ok(result), + Err(e) => { + eprintln!( + "โš ๏ธ Operation failed (attempt {}/{}): {}", + attempt + 1, + $self.config.max_retries + 1, + e + ); + last_error = Some(e); + $self.transport = None; + if attempt < $self.config.max_retries { + let wait = delay_with_jitter(delay); + eprintln!(" Retrying in {:.1}s...", wait.as_secs_f64()); + sleep(wait).await; + delay = std::cmp::min(delay * 2, $self.config.max_delay); + } + } + } + } + } + + Err(last_error.unwrap_or_else(|| "All retry attempts failed".into())) + }}; +} + +/// A transport wrapper that provides automatic retry functionality with exponential backoff +pub struct RetryTransport { + factory: Arc, + transport: Option>, + config: RetryConfig, +} + +impl RetryTransport { + pub fn new(factory: Arc, config: RetryConfig) -> Self { + Self { + factory, + transport: None, + config, + } + } +} + +#[async_trait::async_trait] +impl Transport for RetryTransport { + async fn read( + &mut self, + filename: &Path, + ) -> Result, Box> { + retry_op!(self, |transport| transport.read(filename)) + } + + async fn mkdir(&mut self, path: &Path) -> Result<(), Box> { + retry_op!(self, |transport| transport.mkdir(path)) + } + + async fn write( + &mut self, + filename: &Path, + reader: Box, + file_size: u64, + ) -> Result> { + // For write operations, we can only retry transport creation failures, + // not write operation failures, because the AsyncRead reader is consumed. + let mut delay = self.config.initial_delay; + + for attempt in 0..=self.config.max_retries { + if self.transport.is_none() { + match self.factory.create().await { + Ok(transport) => self.transport = Some(transport), + Err(e) => { + eprintln!( + "โš ๏ธ Transport creation failed for write (attempt {}/{}): {}", + attempt + 1, + self.config.max_retries + 1, + e + ); + if attempt < self.config.max_retries { + let wait = delay_with_jitter(delay); + eprintln!(" Retrying in {:.1}s...", wait.as_secs_f64()); + sleep(wait).await; + delay = std::cmp::min(delay * 2, self.config.max_delay); + continue; + } else { + return Err(e); + } + } + } + } + + if let Some(transport) = &mut self.transport { + return match transport.write(filename, reader, file_size).await { + Ok(result) => Ok(result), + Err(e) => { + self.transport = None; + Err(e) + } + }; + } + } + + Err("Failed to create transport for write operation".into()) + } + + async fn write_last_checksum( + &mut self, + checksum_filename: &Path, + checksum_tree: &ChecksumTree, + ) -> Result> { + // Unlike generic write(), the checksum data can be recreated on each attempt, + // so we can fully retry both transport creation and the write operation. + let mut last_error = None; + let mut delay = self.config.initial_delay; + + for attempt in 0..=self.config.max_retries { + if self.transport.is_none() { + match self.factory.create().await { + Ok(transport) => self.transport = Some(transport), + Err(e) => { + eprintln!( + "โš ๏ธ Transport creation failed for checksum upload (attempt {}/{}): {}", + attempt + 1, + self.config.max_retries + 1, + e + ); + last_error = Some(e); + if attempt < self.config.max_retries { + let wait = delay_with_jitter(delay); + eprintln!(" Retrying in {:.1}s...", wait.as_secs_f64()); + sleep(wait).await; + delay = std::cmp::min(delay * 2, self.config.max_delay); + } + continue; + } + } + } + + if let Some(transport) = &mut self.transport { + let json = checksum_tree.to_gzip()?; + let file_size = json.len() as u64; + let cursor = Cursor::new(json); + match transport + .write(checksum_filename, Box::new(cursor), file_size) + .await + { + Ok(result) => return Ok(result), + Err(e) => { + eprintln!( + "โš ๏ธ Checksum upload failed (attempt {}/{}): {}", + attempt + 1, + self.config.max_retries + 1, + e + ); + last_error = Some(e); + self.transport = None; + if attempt < self.config.max_retries { + let wait = delay_with_jitter(delay); + eprintln!(" Retrying in {:.1}s...", wait.as_secs_f64()); + sleep(wait).await; + delay = std::cmp::min(delay * 2, self.config.max_delay); + } + } + } + } + } + + Err(last_error.unwrap_or_else(|| "All retry attempts failed for checksum upload".into())) + } + + async fn remove( + &mut self, + pathname: &Path, + ) -> Result<(), Box> { + retry_op!(self, |transport| transport.remove(pathname)) + } + + async fn close(mut self: Box) -> Result<(), Box> { + if let Some(transport) = self.transport.take() { + transport.close().await + } else { + Ok(()) + } + } +} + +/// A concrete implementation of TransportFactory that holds the configuration +/// and can recreate transports based on the transport type +pub struct ConfigBasedTransportFactory { + args: Args, +} + +impl ConfigBasedTransportFactory { + pub fn new(args: Args) -> Self { + Self { args } + } +} + +#[async_trait::async_trait] +impl TransportFactory for ConfigBasedTransportFactory { + async fn create( + &self, + ) -> Result, Box> { + use crate::config::TransportType; + use crate::transport::{ + dry::DryTransport, ftp::Ftp, local::LocalFilesystem, s3::AwsS3, sftp::SFtp, + }; + + Ok(match &self.args.transport { + TransportType::Ftp { + ftp_host, + ftp_user, + ftp_pass, + ftp_dir, + use_tls, + } => Box::new( + Ftp::new(ftp_host, ftp_user, ftp_pass, ftp_dir) + .connect(*use_tls) + .await?, + ), + TransportType::Sftp { + host, + user, + pass, + dir, + } => Box::new(SFtp::new(host, user, pass, dir).await?), + TransportType::Local { destination } => Box::new(LocalFilesystem::new(destination)), + TransportType::S3 { + bucket, + region, + access_key, + secret_key, + storage_class, + directory, + } => Box::new(AwsS3::new( + bucket, + region, + access_key, + secret_key, + storage_class, + directory.into(), + )?), + TransportType::Dry => Box::new(DryTransport), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + struct MockTransport; + + #[async_trait::async_trait] + impl Transport for MockTransport { + async fn read( + &mut self, + _filename: &Path, + ) -> Result, Box> { + Ok(vec![1, 2, 3]) + } + async fn mkdir( + &mut self, + _path: &Path, + ) -> Result<(), Box> { + Ok(()) + } + async fn write( + &mut self, + _filename: &Path, + _reader: Box, + _file_size: u64, + ) -> Result> { + Ok(0) + } + async fn remove( + &mut self, + _pathname: &Path, + ) -> Result<(), Box> { + Ok(()) + } + async fn close(self: Box) -> Result<(), Box> { + Ok(()) + } + } + + struct MockFactory { + fail_count: AtomicUsize, + max_failures: usize, + } + + impl MockFactory { + fn new(max_failures: usize) -> Self { + Self { + fail_count: AtomicUsize::new(0), + max_failures, + } + } + } + + #[async_trait::async_trait] + impl TransportFactory for MockFactory { + async fn create( + &self, + ) -> Result, Box> + { + let count = self.fail_count.fetch_add(1, Ordering::SeqCst); + if count < self.max_failures { + Err(format!("mock failure {}", count).into()) + } else { + Ok(Box::new(MockTransport)) + } + } + } + + #[tokio::test] + async fn retry_succeeds_after_failures() { + let factory = Arc::new(MockFactory::new(2)); + let config = RetryConfig::new(3, 1, 1); + let mut transport = RetryTransport::new(factory, config); + + let result = transport.read(Path::new("test.txt")).await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec![1, 2, 3]); + } + + #[tokio::test] + async fn retry_exhausted_returns_error() { + let factory = Arc::new(MockFactory::new(100)); // always fail + let config = RetryConfig::new(2, 1, 1); + let mut transport = RetryTransport::new(factory, config); + + let result = transport.read(Path::new("test.txt")).await; + assert!(result.is_err()); + } + + #[test] + fn retry_config_construction() { + let config = RetryConfig::new(5, 100, 30); + assert_eq!(config.max_retries, 5); + assert_eq!(config.initial_delay, Duration::from_millis(100)); + assert_eq!(config.max_delay, Duration::from_secs(30)); + } + + /// A mock transport whose write() fails a configurable number of times before succeeding. + struct FailingWriteTransport { + write_fail_count: Arc, + write_max_failures: usize, + } + + #[async_trait::async_trait] + impl Transport for FailingWriteTransport { + async fn read( + &mut self, + _filename: &Path, + ) -> Result, Box> { + Ok(vec![]) + } + async fn mkdir( + &mut self, + _path: &Path, + ) -> Result<(), Box> { + Ok(()) + } + async fn write( + &mut self, + _filename: &Path, + _reader: Box, + _file_size: u64, + ) -> Result> { + let count = self.write_fail_count.fetch_add(1, Ordering::SeqCst); + if count < self.write_max_failures { + Err(format!("write failure {}", count).into()) + } else { + Ok(_file_size) + } + } + async fn remove( + &mut self, + _pathname: &Path, + ) -> Result<(), Box> { + Ok(()) + } + async fn close(self: Box) -> Result<(), Box> { + Ok(()) + } + } + + struct FailingWriteFactory { + write_fail_count: Arc, + write_max_failures: usize, + } + + impl FailingWriteFactory { + fn new(write_max_failures: usize) -> Self { + Self { + write_fail_count: Arc::new(AtomicUsize::new(0)), + write_max_failures, + } + } + } + + #[async_trait::async_trait] + impl TransportFactory for FailingWriteFactory { + async fn create( + &self, + ) -> Result, Box> + { + Ok(Box::new(FailingWriteTransport { + write_fail_count: self.write_fail_count.clone(), + write_max_failures: self.write_max_failures, + })) + } + } + + #[tokio::test] + async fn write_last_checksum_retries_on_write_failure() { + let factory = Arc::new(FailingWriteFactory::new(2)); + let config = RetryConfig::new(3, 1, 1); + let mut transport = RetryTransport::new(factory, config); + let checksum_tree = ChecksumTree::default(); + + let result = transport + .write_last_checksum(Path::new("checksum.gz"), &checksum_tree) + .await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn write_last_checksum_exhausted_returns_error() { + let factory = Arc::new(FailingWriteFactory::new(100)); // always fail + let config = RetryConfig::new(2, 1, 1); + let mut transport = RetryTransport::new(factory, config); + let checksum_tree = ChecksumTree::default(); + + let result = transport + .write_last_checksum(Path::new("checksum.gz"), &checksum_tree) + .await; + assert!(result.is_err()); + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..20c62be --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,75 @@ +use std::sync::atomic::AtomicU64; + +pub trait HumanBytes { + fn to_human_size(self) -> String; +} + +impl HumanBytes for u64 { + fn to_human_size(self) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; + const THRESHOLD: u64 = 1024; + + if self == 0 { + return "0 B".to_string(); + } + + let mut size = self as f64; + let mut unit_index = 0; + + while size >= THRESHOLD as f64 && unit_index < UNITS.len() - 1 { + size /= THRESHOLD as f64; + unit_index += 1; + } + + if unit_index == 0 { + format!("{} {}", self, UNITS[unit_index]) + } else { + format!("{:.1} {}", size, UNITS[unit_index]) + } + } +} + +impl HumanBytes for &AtomicU64 { + fn to_human_size(self) -> String { + self.load(std::sync::atomic::Ordering::SeqCst) + .to_human_size() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zero_bytes() { + assert_eq!(0u64.to_human_size(), "0 B"); + } + + #[test] + fn bytes_below_threshold() { + assert_eq!(512u64.to_human_size(), "512 B"); + assert_eq!(1u64.to_human_size(), "1 B"); + assert_eq!(1023u64.to_human_size(), "1023 B"); + } + + #[test] + fn kilobytes() { + assert_eq!(1024u64.to_human_size(), "1.0 KB"); + assert_eq!(1536u64.to_human_size(), "1.5 KB"); + } + + #[test] + fn megabytes() { + assert_eq!(1_048_576u64.to_human_size(), "1.0 MB"); + } + + #[test] + fn gigabytes() { + assert_eq!(1_073_741_824u64.to_human_size(), "1.0 GB"); + } + + #[test] + fn terabytes() { + assert_eq!(1_099_511_627_776u64.to_human_size(), "1.0 TB"); + } +}