From 6400c258da7efcee9d95af44a14d298a9fd17263 Mon Sep 17 00:00:00 2001 From: Alexey Barabanov Date: Tue, 14 Apr 2026 18:53:15 +0300 Subject: [PATCH] Initial project import --- .gitignore | 6 + Cargo.lock | 1480 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 17 + src/lib.rs | 1357 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 133 +++++ 5 files changed, 2993 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bda015d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +.env +.DS_Store +*.xlsx +*.osheet +*.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5577a51 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1480 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ae_anons" +version = "0.1.0" +dependencies = [ + "csv", + "dotenv", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "urlencoding", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[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 = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +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 = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[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 = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +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", +] + +[[package]] +name = "windows-targets" +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_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", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..be1b9af --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ae_anons" +version = "0.1.0" +edition = "2021" + +[dependencies] +reqwest = { version = "0.11", features = ["json", "multipart"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +thiserror = "2.0.18" +urlencoding = "2.1.3" +tokio = { version = "1.51.1", features = ["full"] } +csv = "1.4.0" +dotenv = "0.15.0" + +[dev-dependencies] +tokio = { version = "1.51.1", features = ["full", "rt-multi-thread"] } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d546317 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,1357 @@ +use reqwest::{ + multipart::{Form, Part}, + Client, ClientBuilder, +}; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde_json::Value; +use std::collections::HashMap; +use std::time::Duration; +use urlencoding; + +/// Основная ошибка библиотеки +#[derive(Debug, thiserror::Error)] +pub enum SynologyError { + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("API error: code {code} - {message:?}")] + Api { code: i64, message: Option }, + + #[error("Invalid response format: {0}")] + InvalidResponseFormat(String), + + #[error("Login required")] + LoginRequired, + + #[error("Not logged in")] + NotLoggedIn, + + #[error("Invalid parameter: {0}")] + InvalidParameter(String), + + #[error("MIME error: {0}")] + Mime(#[from] reqwest::header::InvalidHeaderValue), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("CSV error: {0}")] + Csv(#[from] csv::Error), +} + +/// Результат операций +pub type Result = std::result::Result; + +/// Базовый ответ API +#[derive(Debug, Deserialize)] +struct ApiResponse { + success: bool, + #[serde(default)] + data: Option, + #[serde(default)] + error: Option, +} + +/// Пустой тип для ответов без данных +#[derive(Debug, Default, Deserialize)] +struct EmptyData; + +#[derive(Debug, Deserialize)] +struct ApiError { + code: i64, + #[serde(default)] + #[allow(dead_code)] + errors: Option>, +} + +/// Информация о доступных API +#[derive(Debug, Clone, Default, Deserialize)] +pub struct ApiInfo { + pub path: String, + pub min_version: i32, + pub max_version: i32, +} + +/// Информация о виртуальных файловых системах +#[derive(Debug, Default, Deserialize)] +pub struct VirtualSupport { + #[serde(default)] + pub enable_iso_mount: bool, + #[serde(default)] + pub enable_remote_mount: bool, +} + +/// Информация о File Station +#[derive(Debug, Default, Deserialize)] +pub struct FileStationInfo { + pub is_manager: bool, + pub hostname: String, + pub support_sharing: bool, + #[serde(default)] + pub allow_heic_original: bool, + #[serde(default)] + pub allow_normal_disable_html: bool, + #[serde(default)] + pub enable_list_usergrp: bool, + #[serde(default)] + pub enable_send_email_attachment: bool, + #[serde(default)] + pub enable_view_google: bool, + #[serde(default)] + pub enable_view_microsoft: bool, + #[serde(default)] + pub support_file_request: bool, + #[serde(default)] + pub support_vfs: bool, + #[serde(default)] + pub support_virtual: Option, + #[serde(default)] + pub support_virtual_protocol: Vec, + #[serde(default)] + pub system_codepage: String, + #[serde(default)] + pub uid: u64, +} + +/// Объект файла/папки +#[derive(Debug, Clone, Default, Deserialize)] +pub struct FileInfo { + pub path: String, + pub name: String, + pub isdir: bool, + #[serde(default)] + pub additional: Option, +} + +/// Дополнительная информация о файле +#[derive(Debug, Clone, Default, Deserialize)] +pub struct FileAdditional { + pub real_path: Option, + pub size: Option, + #[serde(default)] + pub owner: Option, + #[serde(default)] + pub time: Option, + #[serde(default)] + pub perm: Option, + pub r#type: Option, +} + +/// Информация о владельце +#[derive(Debug, Clone, Default, Deserialize)] +pub struct OwnerInfo { + pub user: String, + pub group: String, + pub uid: u32, + pub gid: u32, +} + +/// Информация о времени +#[derive(Debug, Clone, Default, Deserialize)] +pub struct TimeInfo { + pub atime: u64, + pub mtime: u64, + pub ctime: u64, + pub crtime: u64, +} + +/// Информация о правах доступа +#[derive(Debug, Clone, Default, Deserialize)] +pub struct PermInfo { + pub posix: u32, + pub is_acl_mode: bool, + #[serde(default)] + pub acl: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct AclInfo { + pub append: bool, + pub del: bool, + pub exec: bool, + pub read: bool, + pub write: bool, +} + +/// Общая папка +#[derive(Debug, Default, Deserialize)] +pub struct Share { + pub path: String, + pub name: String, + #[serde(default)] + pub additional: Option, +} + +/// Список файлов +#[derive(Debug, Default, Deserialize)] +pub struct FileList { + pub total: i32, + pub offset: i32, + pub files: Vec, +} + +/// Список общих папок +#[derive(Debug, Default, Deserialize)] +pub struct ShareList { + pub total: i32, + pub offset: i32, + pub shares: Vec, +} + +/// Информация о задаче (для асинхронных операций) +#[derive(Debug, Default, Deserialize)] +pub struct TaskInfo { + pub taskid: String, +} + +/// Статус задачи +#[derive(Debug, Default, Deserialize)] +pub struct TaskStatus { + pub finished: bool, + #[serde(default)] + pub progress: Option, + #[serde(default)] + pub total: Option, + #[serde(default)] + pub processed_size: Option, + #[serde(default)] + pub processed_num: Option, + #[serde(default)] + pub path: Option, + #[serde(default)] + pub processing_path: Option, + #[serde(default)] + pub dest_folder_path: Option, +} + +/// Результат поиска +#[derive(Debug, Default, Deserialize)] +pub struct SearchResult { + pub total: i32, + pub offset: i32, + pub finished: bool, + pub files: Vec, +} + +/// Информация о шаринге +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SharingLink { + pub id: String, + pub url: String, + pub link_owner: String, + pub path: String, + pub is_folder: bool, + pub has_password: bool, + pub date_expired: String, + pub date_available: String, + pub status: String, +} + +/// Созданная ссылка +#[derive(Debug, Clone, Default, Deserialize)] +pub struct CreatedLink { + pub path: String, + pub url: String, + pub id: String, + pub qrcode: Option, + pub error: i32, +} + +/// Параметры сортировки +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SortBy { + Name, + Size, + User, + Group, + MTime, + ATime, + CTime, + Crtime, + Posix, + Type, +} + +impl SortBy { + fn as_str(&self) -> &'static str { + match self { + SortBy::Name => "name", + SortBy::Size => "size", + SortBy::User => "user", + SortBy::Group => "group", + SortBy::MTime => "mtime", + SortBy::ATime => "atime", + SortBy::CTime => "ctime", + SortBy::Crtime => "crtime", + SortBy::Posix => "posix", + SortBy::Type => "type", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SortDirection { + Asc, + Desc, +} + +impl SortDirection { + fn as_str(&self) -> &'static str { + match self { + SortDirection::Asc => "asc", + SortDirection::Desc => "desc", + } + } +} + +/// Параметры для list_files +#[derive(Default)] +pub struct ListFilesOptions { + pub offset: Option, + pub limit: Option, + pub sort_by: Option, + pub sort_direction: Option, + pub pattern: Option, + pub file_type: Option, + pub additional: Option>, +} + +impl ListFilesOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn offset(mut self, offset: i32) -> Self { + self.offset = Some(offset); + self + } + + pub fn limit(mut self, limit: i32) -> Self { + self.limit = Some(limit); + self + } + + pub fn sort_by(mut self, sort_by: SortBy) -> Self { + self.sort_by = Some(sort_by); + self + } + + pub fn sort_direction(mut self, sort_direction: SortDirection) -> Self { + self.sort_direction = Some(sort_direction); + self + } + + pub fn pattern(mut self, pattern: &str) -> Self { + self.pattern = Some(pattern.to_string()); + self + } + + pub fn file_type(mut self, file_type: &str) -> Self { + self.file_type = Some(file_type.to_string()); + self + } + + pub fn additional(mut self, additional: &[&str]) -> Self { + self.additional = Some(additional.iter().map(|&s| s.to_string()).collect()); + self + } +} + +/// Параметры для list_shares +#[derive(Default)] +pub struct ListSharesOptions { + pub offset: Option, + pub limit: Option, + pub sort_by: Option, + pub sort_direction: Option, + pub only_writable: Option, + pub additional: Option>, +} + +impl ListSharesOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn offset(mut self, offset: i32) -> Self { + self.offset = Some(offset); + self + } + + pub fn limit(mut self, limit: i32) -> Self { + self.limit = Some(limit); + self + } + + pub fn sort_by(mut self, sort_by: SortBy) -> Self { + self.sort_by = Some(sort_by); + self + } + + pub fn sort_direction(mut self, sort_direction: SortDirection) -> Self { + self.sort_direction = Some(sort_direction); + self + } + + pub fn only_writable(mut self, only_writable: bool) -> Self { + self.only_writable = Some(only_writable); + self + } + + pub fn additional(mut self, additional: &[&str]) -> Self { + self.additional = Some(additional.iter().map(|&s| s.to_string()).collect()); + self + } +} + +/// Клиент для работы с Synology API +pub struct SynologyClient { + client: Client, + base_url: String, + sid: Option, + session_name: String, +} + +impl SynologyClient { + /// Создание нового клиента + pub fn new(base_url: &str) -> Self { + let client = ClientBuilder::new() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client"); + + Self { + client, + base_url: base_url.trim_end_matches('/').to_string(), + sid: None, + session_name: "FileStation".to_string(), + } + } + + /// Установка имени сессии + pub fn with_session_name(mut self, name: &str) -> Self { + self.session_name = name.to_string(); + self + } + + /// Проверка авторизации + pub fn is_authenticated(&self) -> bool { + self.sid.is_some() + } + + /// Выполнение запроса к API (с возвратом данных) + async fn request( + &self, + path: &str, + params: &[(&str, String)], + ) -> Result { + let mut url = format!("{}/webapi/{}", self.base_url, path); + + let mut param_parts: Vec = Vec::new(); + for (key, value) in params { + param_parts.push(format!("{}={}", key, urlencoding::encode(value))); + } + if !param_parts.is_empty() { + url = format!("{}?{}", url, param_parts.join("&")); + } + + let debug = std::env::var("SYNO_DEBUG").unwrap_or_default() == "1"; + if debug { + eprintln!("[DEBUG] Request URL: {}", url); + } + + let response = self.client.get(&url).send().await?; + + let status = response.status(); + if !status.is_success() { + let error_text = response.text().await?; + if debug { + eprintln!("[DEBUG] Error response: {}", error_text); + } + return Err(SynologyError::Api { + code: status.as_u16() as i64, + message: Some(error_text), + }); + } + + let response_text = response.text().await?; + + if debug { + eprintln!("[DEBUG] Response: {}", response_text); + } + + let api_response: ApiResponse = serde_json::from_str(&response_text) + .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; + + if api_response.success { + if std::any::type_name::() == std::any::type_name::() { + Ok(T::default()) + } else { + api_response.data.ok_or_else(|| { + SynologyError::InvalidResponseFormat("Missing data field".to_string()) + }) + } + } else { + let code = api_response.error.as_ref().map(|e| e.code).unwrap_or(-1); + Err(SynologyError::Api { + code, + message: None, + }) + } + } + + /// Выполнение запроса с SID + async fn request_auth( + &self, + path: &str, + params: Vec<(&str, String)>, + ) -> Result { + let mut all_params = params; + if let Some(sid) = &self.sid { + all_params.push(("_sid", sid.clone())); + } + self.request(path, &all_params).await + } + + /// Выполнение запроса без возврата данных (только проверка success) + async fn request_auth_void(&self, path: &str, params: Vec<(&str, String)>) -> Result<()> { + let _: EmptyData = self.request_auth(path, params).await?; + Ok(()) + } + + /// Получение информации о доступных API + pub async fn get_api_info(&self, apis: &[&str]) -> Result> { + let query = if apis.is_empty() { + "all".to_string() + } else { + apis.join(",") + }; + + let response: Value = self + .request( + "query.cgi", + &[ + ("api", "SYNO.API.Info".to_string()), + ("version", "1".to_string()), + ("method", "query".to_string()), + ("query", query), + ], + ) + .await?; + + let result: HashMap = serde_json::from_value(response) + .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; + + Ok(result) + } + + /// Вход в систему + pub async fn login(&mut self, account: &str, password: &str) -> Result { + let response: HashMap = self + .request( + "auth.cgi", + &[ + ("api", "SYNO.API.Auth".to_string()), + ("version", "3".to_string()), + ("method", "login".to_string()), + ("account", account.to_string()), + ("passwd", password.to_string()), + ("session", self.session_name.clone()), + ("format", "sid".to_string()), + ], + ) + .await?; + + let sid = response + .get("sid") + .ok_or_else(|| { + SynologyError::InvalidResponseFormat("Missing sid in response".to_string()) + })? + .clone(); + + self.sid = Some(sid.clone()); + Ok(sid) + } + + /// Выход из системы + pub async fn logout(&mut self) -> Result<()> { + if self.sid.is_none() { + return Ok(()); + } + + self.request_auth_void( + "auth.cgi", + vec![ + ("api", "SYNO.API.Auth".to_string()), + ("version", "1".to_string()), + ("method", "logout".to_string()), + ("session", self.session_name.clone()), + ], + ) + .await?; + + self.sid = None; + Ok(()) + } + + /// Получение информации о File Station + pub async fn get_info(&self) -> Result { + self.request_auth( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.Info".to_string()), + ("version", "2".to_string()), + ("method", "get".to_string()), + ], + ) + .await + } + + /// Получение списка общих папок + pub async fn list_shares(&self, options: ListSharesOptions) -> Result { + let mut params = vec![ + ("api", "SYNO.FileStation.List".to_string()), + ("version", "2".to_string()), + ("method", "list_share".to_string()), + ]; + + if let Some(offset) = options.offset { + params.push(("offset", offset.to_string())); + } + if let Some(limit) = options.limit { + params.push(("limit", limit.to_string())); + } + if let Some(sort_by) = options.sort_by { + params.push(("sort_by", sort_by.as_str().to_string())); + } + if let Some(sort_direction) = options.sort_direction { + params.push(("sort_direction", sort_direction.as_str().to_string())); + } + if let Some(only_writable) = options.only_writable { + params.push(("onlywritable", only_writable.to_string())); + } + if let Some(additional) = options.additional { + let additional_str = additional + .iter() + .map(|s| format!("\"{}\"", s)) + .collect::>() + .join(","); + params.push(("additional", format!("[{}]", additional_str))); + } + + self.request_auth("entry.cgi", params).await + } + + /// Получение списка файлов в папке + pub async fn list_files( + &self, + folder_path: &str, + options: ListFilesOptions, + ) -> Result { + let mut params = vec![ + ("api", "SYNO.FileStation.List".to_string()), + ("version", "2".to_string()), + ("method", "list".to_string()), + ("folder_path", folder_path.to_string()), + ]; + + if let Some(offset) = options.offset { + params.push(("offset", offset.to_string())); + } + if let Some(limit) = options.limit { + params.push(("limit", limit.to_string())); + } + if let Some(sort_by) = options.sort_by { + params.push(("sort_by", sort_by.as_str().to_string())); + } + if let Some(sort_direction) = options.sort_direction { + params.push(("sort_direction", sort_direction.as_str().to_string())); + } + if let Some(pattern) = options.pattern { + params.push(("pattern", pattern)); + } + if let Some(file_type) = options.file_type { + params.push(("filetype", file_type)); + } + if let Some(additional) = options.additional { + let additional_str = additional + .iter() + .map(|s| format!("\"{}\"", s)) + .collect::>() + .join(","); + params.push(("additional", format!("[{}]", additional_str))); + } + + self.request_auth("entry.cgi", params).await + } + + /// Создание папки + pub async fn create_folder( + &self, + folder_path: &str, + name: &str, + force_parent: Option, + ) -> Result> { + let mut params = vec![ + ("api", "SYNO.FileStation.CreateFolder".to_string()), + ("version", "2".to_string()), + ("method", "create".to_string()), + ("folder_path", format!("[\"{}\"]", folder_path)), + ("name", format!("[\"{}\"]", name)), + ]; + + if let Some(force_parent) = force_parent { + params.push(("force_parent", force_parent.to_string())); + } + + let response: HashMap> = + self.request_auth("entry.cgi", params).await?; + + Ok(response.get("folders").cloned().unwrap_or_default()) + } + + /// Переименование файла/папки + pub async fn rename(&self, path: &str, new_name: &str) -> Result> { + let response: HashMap> = self + .request_auth( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.Rename".to_string()), + ("version", "2".to_string()), + ("method", "rename".to_string()), + ("path", format!("[\"{}\"]", path)), + ("name", format!("[\"{}\"]", new_name)), + ], + ) + .await?; + + Ok(response.get("files").cloned().unwrap_or_default()) + } + + /// Копирование файлов/папок (асинхронное) + pub async fn copy_start( + &self, + paths: &[&str], + dest_folder_path: &str, + overwrite: Option, + ) -> Result { + let paths_str = paths + .iter() + .map(|&p| format!("\"{}\"", p)) + .collect::>() + .join(","); + + let mut params = vec![ + ("api", "SYNO.FileStation.CopyMove".to_string()), + ("version", "3".to_string()), + ("method", "start".to_string()), + ("path", format!("[{}]", paths_str)), + ("dest_folder_path", dest_folder_path.to_string()), + ("remove_src", "false".to_string()), + ]; + + if let Some(overwrite) = overwrite { + params.push(("overwrite", overwrite.to_string())); + } + + let response: TaskInfo = self.request_auth("entry.cgi", params).await?; + Ok(response.taskid) + } + + /// Перемещение файлов/папок (асинхронное) + pub async fn move_start( + &self, + paths: &[&str], + dest_folder_path: &str, + overwrite: Option, + ) -> Result { + let paths_str = paths + .iter() + .map(|&p| format!("\"{}\"", p)) + .collect::>() + .join(","); + + let mut params = vec![ + ("api", "SYNO.FileStation.CopyMove".to_string()), + ("version", "3".to_string()), + ("method", "start".to_string()), + ("path", format!("[{}]", paths_str)), + ("dest_folder_path", dest_folder_path.to_string()), + ("remove_src", "true".to_string()), + ]; + + if let Some(overwrite) = overwrite { + params.push(("overwrite", overwrite.to_string())); + } + + let response: TaskInfo = self.request_auth("entry.cgi", params).await?; + Ok(response.taskid) + } + + /// Получение статуса задачи копирования/перемещения + pub async fn get_task_status(&self, task_id: &str) -> Result { + self.request_auth( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.CopyMove".to_string()), + ("version", "3".to_string()), + ("method", "status".to_string()), + ("taskid", task_id.to_string()), + ], + ) + .await + } + + /// Удаление файлов/папок (блокирующее) + pub async fn delete(&self, paths: &[&str], recursive: Option) -> Result<()> { + let paths_str = paths + .iter() + .map(|&p| format!("\"{}\"", p)) + .collect::>() + .join(","); + + let mut params = vec![ + ("api", "SYNO.FileStation.Delete".to_string()), + ("version", "2".to_string()), + ("method", "delete".to_string()), + ("path", format!("[{}]", paths_str)), + ]; + + if let Some(recursive) = recursive { + params.push(("recursive", recursive.to_string())); + } + + self.request_auth_void("entry.cgi", params).await?; + Ok(()) + } + + /// Удаление файлов/папок (асинхронное) + pub async fn delete_start(&self, paths: &[&str], recursive: Option) -> Result { + let paths_str = paths + .iter() + .map(|&p| format!("\"{}\"", p)) + .collect::>() + .join(","); + + let mut params = vec![ + ("api", "SYNO.FileStation.Delete".to_string()), + ("version", "2".to_string()), + ("method", "start".to_string()), + ("path", format!("[{}]", paths_str)), + ]; + + if let Some(recursive) = recursive { + params.push(("recursive", recursive.to_string())); + } + + let response: TaskInfo = self.request_auth("entry.cgi", params).await?; + Ok(response.taskid) + } + + /// Получение статуса удаления + pub async fn get_delete_status(&self, task_id: &str) -> Result { + self.request_auth( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.Delete".to_string()), + ("version", "2".to_string()), + ("method", "status".to_string()), + ("taskid", task_id.to_string()), + ], + ) + .await + } + + /// Поиск файлов + pub async fn search_start( + &self, + folder_paths: &[&str], + recursive: Option, + pattern: Option<&str>, + file_type: Option<&str>, + extension: Option<&str>, + ) -> Result { + let paths_str = folder_paths + .iter() + .map(|&p| format!("\"{}\"", p)) + .collect::>() + .join(","); + + let mut params = vec![ + ("api", "SYNO.FileStation.Search".to_string()), + ("version", "2".to_string()), + ("method", "start".to_string()), + ("folder_path", format!("[{}]", paths_str)), + ]; + + if let Some(recursive) = recursive { + params.push(("recursive", recursive.to_string())); + } + if let Some(pattern) = pattern { + params.push(("pattern", pattern.to_string())); + } + if let Some(file_type) = file_type { + params.push(("filetype", file_type.to_string())); + } + if let Some(extension) = extension { + params.push(("extension", extension.to_string())); + } + + let response: TaskInfo = self.request_auth("entry.cgi", params).await?; + Ok(response.taskid) + } + + /// Получение результатов поиска + pub async fn search_list( + &self, + task_id: &str, + offset: Option, + limit: Option, + additional: Option<&[&str]>, + ) -> Result { + let mut params = vec![ + ("api", "SYNO.FileStation.Search".to_string()), + ("version", "2".to_string()), + ("method", "list".to_string()), + ("taskid", task_id.to_string()), + ]; + + if let Some(offset) = offset { + params.push(("offset", offset.to_string())); + } + if let Some(limit) = limit { + params.push(("limit", limit.to_string())); + } + if let Some(additional) = additional { + let additional_str = additional + .iter() + .map(|&s| format!("\"{}\"", s)) + .collect::>() + .join(","); + params.push(("additional", format!("[{}]", additional_str))); + } + + self.request_auth("entry.cgi", params).await + } + + /// Остановка поиска + pub async fn search_stop(&self, task_id: &str) -> Result<()> { + self.request_auth_void( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.Search".to_string()), + ("version", "2".to_string()), + ("method", "stop".to_string()), + ("taskid", task_id.to_string()), + ], + ) + .await?; + Ok(()) + } + + /// Очистка результатов поиска + pub async fn search_clean(&self, task_id: &str) -> Result<()> { + self.request_auth_void( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.Search".to_string()), + ("version", "2".to_string()), + ("method", "clean".to_string()), + ("taskid", task_id.to_string()), + ], + ) + .await?; + Ok(()) + } + + /// Создание ссылки для скачивания + pub async fn create_sharing_link( + &self, + path: &str, + password: Option<&str>, + date_expired: Option<&str>, + date_available: Option<&str>, + ) -> Result { + let mut params = vec![ + ("api", "SYNO.FileStation.Sharing".to_string()), + ("version", "3".to_string()), + ("method", "create".to_string()), + ("path", path.to_string()), + ]; + + if let Some(password) = password { + params.push(("password", password.to_string())); + } + if let Some(date_expired) = date_expired { + params.push(("date_expired", date_expired.to_string())); + } + if let Some(date_available) = date_available { + params.push(("date_available", date_available.to_string())); + } + + let response: HashMap> = + self.request_auth("entry.cgi", params).await?; + + response + .get("links") + .and_then(|links| links.first().cloned()) + .ok_or_else(|| { + SynologyError::InvalidResponseFormat("Missing links in response".to_string()) + }) + } + + /// Получение списка ссылок + pub async fn list_sharing_links( + &self, + offset: Option, + limit: Option, + ) -> Result> { + let mut params = vec![ + ("api", "SYNO.FileStation.Sharing".to_string()), + ("version", "3".to_string()), + ("method", "list".to_string()), + ]; + + if let Some(offset) = offset { + params.push(("offset", offset.to_string())); + } + if let Some(limit) = limit { + params.push(("limit", limit.to_string())); + } + + let response: HashMap = self.request_auth("entry.cgi", params).await?; + + let links = response + .get("links") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_default(); + + Ok(links) + } + + /// Удаление ссылки + pub async fn delete_sharing_link(&self, id: &str) -> Result<()> { + self.request_auth_void( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.Sharing".to_string()), + ("version", "3".to_string()), + ("method", "delete".to_string()), + ("id", id.to_string()), + ], + ) + .await?; + Ok(()) + } + + /// Получение размера папки (асинхронное) + pub async fn get_dir_size_start(&self, paths: &[&str]) -> Result { + let paths_str = paths + .iter() + .map(|&p| format!("\"{}\"", p)) + .collect::>() + .join(","); + + let response: TaskInfo = self + .request_auth( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.DirSize".to_string()), + ("version", "2".to_string()), + ("method", "start".to_string()), + ("path", format!("[{}]", paths_str)), + ], + ) + .await?; + + Ok(response.taskid) + } + + /// Получение статуса вычисления размера + pub async fn get_dir_size_status(&self, task_id: &str) -> Result { + self.request_auth( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.DirSize".to_string()), + ("version", "2".to_string()), + ("method", "status".to_string()), + ("taskid", task_id.to_string()), + ], + ) + .await + } + + /// Архивирование файлов + pub async fn compress_start( + &self, + paths: &[&str], + dest_file_path: &str, + format: Option<&str>, + password: Option<&str>, + ) -> Result { + let paths_str = paths + .iter() + .map(|&p| format!("\"{}\"", p)) + .collect::>() + .join(","); + + let mut params = vec![ + ("api", "SYNO.FileStation.Compress".to_string()), + ("version", "3".to_string()), + ("method", "start".to_string()), + ("path", format!("[{}]", paths_str)), + ("dest_file_path", dest_file_path.to_string()), + ]; + + if let Some(format) = format { + params.push(("format", format.to_string())); + } + if let Some(password) = password { + params.push(("password", password.to_string())); + } + + let response: TaskInfo = self.request_auth("entry.cgi", params).await?; + Ok(response.taskid) + } + + /// Получение статуса архивации + pub async fn get_compress_status(&self, task_id: &str) -> Result { + self.request_auth( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.Compress".to_string()), + ("version", "3".to_string()), + ("method", "status".to_string()), + ("taskid", task_id.to_string()), + ], + ) + .await + } + + /// Извлечение архива + pub async fn extract_start( + &self, + file_path: &str, + dest_folder_path: &str, + overwrite: Option, + password: Option<&str>, + ) -> Result { + let mut params = vec![ + ("api", "SYNO.FileStation.Extract".to_string()), + ("version", "2".to_string()), + ("method", "start".to_string()), + ("file_path", file_path.to_string()), + ("dest_folder_path", dest_folder_path.to_string()), + ]; + + if let Some(overwrite) = overwrite { + params.push(("overwrite", overwrite.to_string())); + } + if let Some(password) = password { + params.push(("password", password.to_string())); + } + + let response: TaskInfo = self.request_auth("entry.cgi", params).await?; + Ok(response.taskid) + } + + /// Получение статуса извлечения + pub async fn get_extract_status(&self, task_id: &str) -> Result { + self.request_auth( + "entry.cgi", + vec![ + ("api", "SYNO.FileStation.Extract".to_string()), + ("version", "2".to_string()), + ("method", "status".to_string()), + ("taskid", task_id.to_string()), + ], + ) + .await + } + + /// Скачивание файла (возвращает бинарные данные) + pub async fn download(&self, path: &str) -> Result> { + let url = format!( + "{}/webapi/entry.cgi?api=SYNO.FileStation.Download&version=2&method=download&path={}&mode=open", + self.base_url, path + ); + let mut request_builder = self.client.get(&url); + if let Some(sid) = &self.sid { + request_builder = request_builder.query(&[("_sid", sid)]); + } + let response = request_builder.send().await?; + let status = response.status(); + if !status.is_success() { + let error_text = response.text().await?; + return Err(SynologyError::Api { + code: status.as_u16() as i64, + message: Some(error_text), + }); + } + Ok(response.bytes().await?.to_vec()) + } + + /// Загрузка файла (multipart) + pub async fn upload( + &self, + folder_path: &str, + filename: &str, + data: Vec, + create_parents: Option, + overwrite: Option, + ) -> Result<()> { + let mut form = Form::new() + .text("api".to_string(), "SYNO.FileStation.Upload".to_string()) + .text("version".to_string(), "2".to_string()) + .text("method".to_string(), "upload".to_string()) + .text("path".to_string(), folder_path.to_string()); + + if let Some(create_parents) = create_parents { + form = form.text("create_parents".to_string(), create_parents.to_string()); + } + if let Some(overwrite) = overwrite { + form = form.text("overwrite".to_string(), overwrite.to_string()); + } + + let file_part = Part::bytes(data) + .file_name(filename.to_string()) + .mime_str("application/octet-stream")?; + + form = form.part("file", file_part); + + let url = format!("{}/webapi/entry.cgi", self.base_url); + let mut request_builder = self.client.post(&url).multipart(form); + + if let Some(sid) = &self.sid { + request_builder = request_builder.query(&[("_sid", sid)]); + } + + let response = request_builder.send().await?; + let api_response: ApiResponse = response.json().await?; + + if api_response.success { + Ok(()) + } else { + let code = api_response.error.as_ref().map(|e| e.code).unwrap_or(-1); + Err(SynologyError::Api { + code, + message: None, + }) + } + } + + /// Поиск файла по имени и пути + pub async fn search_file_by_name_and_path(&self, name: &str, path: &str) -> Result { + let url = format!("{}/webapi/entry.cgi", self.base_url); + let data = format!( + "api=SYNO.SynologyDrive.Files&method=search&version=2&keyword={}&path={}&_sid={}", + urlencoding::encode(name), + urlencoding::encode(path), + self.sid.as_ref().unwrap_or(&String::new()) + ); + + println!(" 🔍 Searching for file: {} in path: {}", name, path); + + let response = self + .client + .post(&url) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(data) + .send() + .await?; + + let response_text = response.text().await?; + + let json: Value = serde_json::from_str(&response_text) + .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; + + if json.get("success").and_then(|s| s.as_bool()) != Some(true) { + let code = json["error"]["code"].as_i64().unwrap_or(-1); + return Err(SynologyError::Api { + code, + message: Some(format!("{:?}", json.get("error"))), + }); + } + + Ok(json) + } + + /// Поиск файла по имени (без ограничения по пути) + pub async fn search_file_by_name(&self, name: &str) -> Result { + let url = format!("{}/webapi/entry.cgi", self.base_url); + let data = format!( + "api=SYNO.SynologyDrive.Files&method=search&version=2&keyword={}&_sid={}", + urlencoding::encode(name), + self.sid.as_ref().unwrap_or(&String::new()) + ); + + println!(" 🔍 Searching for file: {}", name); + + let response = self + .client + .post(&url) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(data) + .send() + .await?; + + let response_text = response.text().await?; + + let json: Value = serde_json::from_str(&response_text) + .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; + + if json.get("success").and_then(|s| s.as_bool()) != Some(true) { + let code = json["error"]["code"].as_i64().unwrap_or(-1); + return Err(SynologyError::Api { + code, + message: Some(format!("{:?}", json.get("error"))), + }); + } + + Ok(json) + } + + /// Экспорт файла Synology Office по ID + pub async fn export_by_file_id(&self, file_id: &str, file_name: &str) -> Result> { + let export_endpoint = file_name.replace(".osheet", ".xlsx"); + let endpoint = format!("entry.cgi/{}", export_endpoint); + + let params = format!( + "api=SYNO.Office.Export&method=download&version=1&path=id:{}&_sid={}", + file_id, + self.sid.as_ref().unwrap_or(&String::new()) + ); + + let url = format!("{}/webapi/{}", self.base_url, endpoint); + println!(" 📤 Export URL: {}", url); + + let response = self + .client + .post(&url) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(params) + .send() + .await?; + + let status = response.status(); + + if !status.is_success() { + let error_text = response.text().await?; + return Err(SynologyError::Api { + code: status.as_u16() as i64, + message: Some(error_text), + }); + } + + let bytes = response.bytes().await?; + println!(" ✅ Got {} bytes", bytes.len()); + + Ok(bytes.to_vec()) + } +} + +impl Drop for SynologyClient { + fn drop(&mut self) {} +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cb5d1c6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,133 @@ +use dotenv::dotenv; +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use serde_json::Value; + +use ae_anons::{SynologyClient, SynologyError}; + +#[tokio::main] +async fn main() -> Result<(), SynologyError> { + dotenv().ok(); + + let nas_fqdn = env::var("NAS_FQDN").expect("❌ Переменная NAS_FQDN не задана в .env"); + let nas_user = env::var("NAS_USER").expect("❌ Переменная NAS_USER не задана в .env"); + let nas_pass = env::var("NAS_PASS").expect("❌ Переменная NAS_PASS не задана в .env"); + let nas_file = env::var("NAS_FILE").expect("❌ Переменная NAS_FILE не задана в .env"); + + let mut client = SynologyClient::new(&nas_fqdn); + + client.login(&nas_user, &nas_pass).await?; + println!("✅ Logged in"); + + let info = client.get_info().await?; + println!("📡 Connected to: {}", info.hostname); + + // Получаем имя файла и ожидаемый путь + let file_name_full = Path::new(&nas_file).file_name().unwrap().to_str().unwrap(); + let search_name = file_name_full.replace(".osheet", ""); + let expected_path = Path::new(&nas_file).parent().unwrap().to_str().unwrap(); + + // Преобразуем путь в формат API (Team Folder -> team-folders, убираем пробелы, приводим к нижнему регистру) + let api_format_path = expected_path + .replace("Team Folder", "team-folders") + .replace(" ", ""); + + let full_expected_path_api = format!("{}/{}", api_format_path, file_name_full); + + println!("\n🔍 Looking for exact file: {}", full_expected_path_api); + println!(" (converted from: {})", nas_file); + + // Ищем файл по имени + let search_result = client.search_file_by_name(&search_name).await?; + + // Извлекаем все найденные файлы + let items = search_result["data"]["items"].as_array() + .ok_or_else(|| SynologyError::InvalidResponseFormat("No items in search result".to_string()))?; + + if items.is_empty() { + return Err(SynologyError::Api { + code: 404, + message: Some(format!("File '{}' not found", file_name_full)), + }); + } + + // Ищем файл с точным совпадением пути (в формате API) + let exact_match: Vec<&Value> = items.iter() + .filter(|item| { + let display_path = item.get("display_path") + .and_then(|p| p.as_str()) + .unwrap_or(""); + display_path == full_expected_path_api + }) + .collect(); + + if exact_match.is_empty() { + println!("\n⚠️ File not found at exact path: {}", full_expected_path_api); + println!(" Found {} file(s) with similar name:", items.len()); + + for (i, item) in items.iter().enumerate() { + let name = item.get("name") + .and_then(|n| n.as_str()) + .unwrap_or("unknown"); + let display_path = item.get("display_path") + .and_then(|p| p.as_str()) + .unwrap_or("unknown"); + let file_id = item.get("file_id") + .and_then(|id| id.as_str()) + .unwrap_or("unknown"); + println!(" {}. {} (at: {}, ID: {})", i + 1, name, display_path, file_id); + } + + return Err(SynologyError::Api { + code: 400, + message: Some(format!("File not found at: {}", full_expected_path_api)), + }); + } + + if exact_match.len() > 1 { + println!("\n⚠️ Found {} files at the same location!", exact_match.len()); + return Err(SynologyError::Api { + code: 400, + message: Some("Multiple files found at the same location".to_string()), + }); + } + + // Берём единственный подходящий файл + let file = exact_match[0]; + let file_id = file["file_id"] + .as_str() + .ok_or_else(|| SynologyError::InvalidResponseFormat("Missing file_id".to_string()))?; + + let actual_file_name = file["name"] + .as_str() + .ok_or_else(|| SynologyError::InvalidResponseFormat("Missing name".to_string()))?; + + let actual_path = file["display_path"] + .as_str() + .unwrap_or("unknown"); + + println!(" 📄 Found exact match: {} at {}", actual_file_name, actual_path); + println!(" 📄 File ID: {}", file_id); + + // Экспортируем файл + println!("\n📊 Exporting file..."); + + match client.export_by_file_id(file_id, actual_file_name).await { + Ok(data) => { + let output_path = format!("./{}_export.xlsx", search_name); + let mut file = File::create(&output_path)?; + file.write_all(&data)?; + println!(" ✅ Exported to: {} ({} bytes)", output_path, data.len()); + } + Err(e) => { + println!(" ❌ Export failed: {}", e); + } + } + + client.logout().await?; + println!("\n👋 Logged out"); + + Ok(()) +} \ No newline at end of file