diff --git a/.gitignore b/.gitignore index 5f420d8..376cbe2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ .DS_Store CLAUDE.md .claude/settings.local.json +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..59714b5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2084 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bendy" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8133e404c8bec821e531f347dab1247bf64f60882826e7228f8ffeb33a35a658" +dependencies = [ + "failure", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +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 = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cid" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" +dependencies = [ + "core2", + "multibase", + "multihash", + "unsigned-varint", +] + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "data-encoding-macro" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" +dependencies = [ + "data-encoding", + "syn 2.0.114", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[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 = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "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 = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[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 = "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 = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[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 = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "metamesh-plugin-fast-full-hash" +version = "1.0.0" +dependencies = [ + "anyhow", + "axum", + "bendy", + "cid", + "crc32fast", + "hex", + "md-5", + "multihash", + "reqwest", + "serde", + "serde_json", + "sha1", + "sha2", + "sha3", + "tempfile", + "thiserror", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "unsigned-varint", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +dependencies = [ + "core2", + "unsigned-varint", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +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 = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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 2.0.114", +] + +[[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_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "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 2.0.114", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[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 = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +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", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[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.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.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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.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 0.52.6", + "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-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 2.0.114", + "synstructure 0.13.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 2.0.114", + "synstructure 0.13.2", +] + +[[package]] +name = "zeroize" +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 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" diff --git a/Cargo.toml b/Cargo.toml index d9df8d0..cf02528 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ bendy = "0.3" # Serialization serde = { version = "1", features = ["derive"] } serde_json = "1" -bincode = "1.3" # HTTP client reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } diff --git a/Dockerfile b/Dockerfile index 9c14657..9cce883 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,37 @@ -# MetaMesh Plugin: full-hash -# Computes SHA-256 hash of the entire file with CSV caching +# MetaMesh Plugin: full-hash (Rust) +# High-performance file hashing with multi-algorithm support -FROM node:20-slim AS builder +FROM rust:1.83-slim AS builder WORKDIR /app -COPY package.json tsconfig.json ./ -RUN npm install + +# Install build dependencies +RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* + +# Copy manifests and build dependencies first (for caching) +COPY Cargo.toml ./ +RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src + +# Copy source and build COPY src/ ./src/ -RUN npm run build +RUN touch src/main.rs && cargo build --release -FROM node:20-slim +FROM debian:bookworm-slim WORKDIR /app -COPY package.json ./ -RUN npm install --omit=dev -COPY --from=builder /app/dist ./dist + +# Install runtime dependencies (curl for healthcheck, ca-certificates for HTTPS) +RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/metamesh-plugin-fast-full-hash /app/plugin + EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ - CMD node -e "require('http').get('http://localhost:8080/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))" -CMD ["node", "dist/index.js"] + CMD curl -f http://localhost:8080/health || exit 1 + +ENV RUST_LOG=info +ENV CACHE_PATH=/cache + +# WebDAV URL for file access (set by container manager) +# Example: http://meta-sort-dev/webdav +ENV WEBDAV_URL= + +CMD ["/app/plugin"] diff --git a/package.json b/package.json deleted file mode 100644 index e6df08a..0000000 --- a/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@metazla/plugin-full-hash", - "version": "1.0.0", - "description": "MetaMesh plugin: Computes SHA-256 hash of the entire file", - "type": "module", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx watch src/index.ts" - }, - "dependencies": { - "crc-32": "^1.2.2", - "csv-parse": "^5.5.5", - "csv-stringify": "^6.4.6", - "fastify": "^5.0.0" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "tsx": "^4.0.0", - "typescript": "^5.3.0" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/src/api/manifest.rs b/src/api/manifest.rs index 0e566ea..554bb34 100644 --- a/src/api/manifest.rs +++ b/src/api/manifest.rs @@ -21,8 +21,8 @@ pub struct ManifestResponse { pub async fn manifest() -> Json { Json(ManifestResponse { - id: "fast-full-hash", - name: "Fast Full File Hash (Rust)", + id: "full-hash", + name: "Full File Hash", version: env!("CARGO_PKG_VERSION"), description: "High-performance parallel hashing with optimized I/O", author: "MetaMesh", diff --git a/src/api/process.rs b/src/api/process.rs index b7427ea..c4b5a62 100644 --- a/src/api/process.rs +++ b/src/api/process.rs @@ -8,7 +8,8 @@ use std::time::Instant; use crate::cache::CacheManager; use crate::client::MetaCoreClient; use crate::config::{HashAlgorithm, SharedConfig}; -use crate::hasher::hash_file; +use crate::hasher::{hash_bytes, hash_file}; +use crate::webdav::WebDavClient; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -114,33 +115,74 @@ enum ProcessResult { Skipped(String), } +/// File source - either local filesystem or WebDAV +enum FileSource { + Local { + path: std::path::PathBuf, + size: u64, + mtime: i64, + }, + WebDav { + data: Vec, + size: u64, + }, +} + async fn do_process_file( state: Arc, req: ProcessRequest, ) -> anyhow::Result { let path = Path::new(&req.file_path); - // Check if file exists - if !path.exists() { - anyhow::bail!("File not found: {}", req.file_path); - } - - // Get file metadata - let metadata = std::fs::metadata(path)?; - let size = metadata.len(); - let mtime = metadata - .modified() - .ok() - .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|d| d.as_secs() as i64) - .unwrap_or(0); - + // Extract filename from path let filename = path .file_name() .and_then(|n| n.to_str()) .unwrap_or("unknown") .to_string(); + // Try to get file - either from local filesystem or WebDAV + let source = if path.exists() { + // Local file available + let metadata = std::fs::metadata(path)?; + let size = metadata.len(); + let mtime = metadata + .modified() + .ok() + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + + tracing::debug!("Using local file: {}", req.file_path); + FileSource::Local { + path: path.to_path_buf(), + size, + mtime, + } + } else { + // Try WebDAV + let webdav_url = std::env::var("WEBDAV_URL") + .map_err(|_| anyhow::anyhow!("File not found locally and WEBDAV_URL not set: {}", req.file_path))?; + + tracing::info!("Fetching via WebDAV: {}", req.file_path); + let client = WebDavClient::new(&webdav_url); + let reader = client.get_file(&req.file_path).await?; + let size = reader.size().unwrap_or(0); + let data = reader.bytes().await?; + + tracing::info!("WebDAV fetch complete: {} bytes", data.len()); + FileSource::WebDav { + data, + size, + } + }; + + // Get file size and mtime for cache lookup + let (size, mtime) = match &source { + FileSource::Local { size, mtime, .. } => (*size, *mtime), + FileSource::WebDav { size, .. } => (*size, 0), // No mtime for WebDAV + }; + // Get config let config = state.config.read().await.clone(); @@ -194,12 +236,20 @@ async fn do_process_file( ); let enabled_set = to_compute.iter().copied().collect(); - let computed = tokio::task::spawn_blocking({ - let path = path.to_path_buf(); - let buffer_size = config.buffer_size; - move || hash_file(&path, &enabled_set, buffer_size) - }) - .await??; + let computed = match source { + FileSource::Local { path, .. } => { + // Use file-based hashing for local files + let buffer_size = config.buffer_size; + tokio::task::spawn_blocking(move || hash_file(&path, &enabled_set, buffer_size)) + .await?? + } + FileSource::WebDav { data, .. } => { + // Use bytes-based hashing for WebDAV data + let filename_clone = filename.clone(); + tokio::task::spawn_blocking(move || hash_bytes(&data, &filename_clone, &enabled_set)) + .await?? + } + }; // Merge computed hashes into results for (key, value) in &computed { @@ -214,10 +264,11 @@ async fn do_process_file( .insert(filename.clone(), size, mtime, all_hashes) .await; - // Save cache periodically (could be optimized with debouncing) - if let Err(e) = state.cache.save().await { - tracing::warn!("Failed to save cache: {}", e); - } + // CRITICAL: Save cache to disk BEFORE reporting done + // This ensures the cache file is written and synced before the callback + state.cache.save().await?; + + tracing::info!("Cache saved for {} ({} hashes)", filename, results.len()); } // Store results to meta-core diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 69d99bf..af0fb3f 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,172 +1,256 @@ use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::{self, File}; -use std::io::{BufReader, BufWriter}; +use std::collections::{HashMap, HashSet}; +use std::fs::{self, File, OpenOptions}; +use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::sync::RwLock; -const CACHE_VERSION: u32 = 1; -const CACHE_FILENAME: &str = "hash_cache.bin"; +use crate::config::HashAlgorithm; -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Cache entry for a single file +#[derive(Debug, Clone)] pub struct CacheEntry { pub filename: String, pub size: u64, pub mtime: i64, - pub hashes: HashMap, + pub hash: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CacheIndex { - pub version: u32, - pub entries: HashMap, +/// Per-algorithm CSV index manager +/// +/// Each algorithm has its own CSV file: index-{algorithm}.csv +/// Format: path,size,mtime,hash +/// +/// Uses append-only writes for efficiency (like meta-hash does) +struct AlgorithmIndex { + file_path: PathBuf, + /// In-memory cache: key = "filename|size|mtime", value = hash + entries: HashMap, + /// Track which entries are new and need to be appended + pending_writes: Vec, } -impl Default for CacheIndex { - fn default() -> Self { +impl AlgorithmIndex { + fn new(cache_dir: &Path, algorithm: &str) -> Self { + let file_path = cache_dir.join(format!("index-{}.csv", algorithm)); Self { - version: CACHE_VERSION, + file_path, entries: HashMap::new(), + pending_writes: Vec::new(), } } -} -impl CacheIndex { - /// Generate cache key from file metadata - pub fn make_key(filename: &str, size: u64, mtime: i64) -> String { - format!("{}-{}-{}", filename, size, mtime) + fn make_key(filename: &str, size: u64, mtime: i64) -> String { + format!("{}|{}|{}", filename, size, mtime) } - /// Get cached entry if it exists and matches file metadata - pub fn get(&self, filename: &str, size: u64, mtime: i64) -> Option<&CacheEntry> { + /// Load existing entries from CSV file + fn load(&mut self) -> Result<()> { + if !self.file_path.exists() { + return Ok(()); + } + + let file = File::open(&self.file_path) + .with_context(|| format!("Failed to open index file: {:?}", self.file_path))?; + let reader = BufReader::new(file); + + let mut first_line = true; + for line in reader.lines() { + let line = line?; + + // Skip header + if first_line { + first_line = false; + if line.starts_with("path,") { + continue; + } + } + + // Parse CSV line: path,size,mtime,hash + let parts: Vec<&str> = line.splitn(4, ',').collect(); + if parts.len() == 4 { + let filename = parts[0].to_string(); + let size: u64 = parts[1].parse().unwrap_or(0); + let mtime: i64 = parts[2].parse().unwrap_or(0); + let hash = parts[3].to_string(); + + let key = Self::make_key(&filename, size, mtime); + self.entries.insert(key, hash); + } + } + + Ok(()) + } + + /// Get cached hash for a file + fn get(&self, filename: &str, size: u64, mtime: i64) -> Option<&String> { let key = Self::make_key(filename, size, mtime); self.entries.get(&key) } - /// Insert or update a cache entry - pub fn insert(&mut self, filename: String, size: u64, mtime: i64, hashes: HashMap) { + /// Add a hash to the cache (queues for append) + fn insert(&mut self, filename: String, size: u64, mtime: i64, hash: String) { let key = Self::make_key(&filename, size, mtime); - self.entries.insert( - key, - CacheEntry { - filename, + + // Only queue for write if not already in cache + if !self.entries.contains_key(&key) { + self.pending_writes.push(CacheEntry { + filename: filename.clone(), size, mtime, - hashes, - }, - ); + hash: hash.clone(), + }); + self.entries.insert(key, hash); + } + } + + /// Append pending entries to CSV file + fn flush(&mut self) -> Result<()> { + if self.pending_writes.is_empty() { + return Ok(()); + } + + // Ensure parent directory exists + if let Some(parent) = self.file_path.parent() { + fs::create_dir_all(parent)?; + } + + // Check if file exists to determine if we need a header + let needs_header = !self.file_path.exists() || fs::metadata(&self.file_path)?.len() == 0; + + // Open file in append mode + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&self.file_path)?; + + // Write header if needed + if needs_header { + writeln!(file, "path,size,mtime,hash")?; + } + + // Append new entries + for entry in &self.pending_writes { + writeln!(file, "{},{},{},{}", entry.filename, entry.size, entry.mtime, entry.hash)?; + } + + // Ensure data is flushed to disk + file.flush()?; + file.sync_all()?; + + let count = self.pending_writes.len(); + self.pending_writes.clear(); + + tracing::debug!("Appended {} entries to {:?}", count, self.file_path); + + Ok(()) } } +/// Cache manager that handles multiple algorithm indexes pub struct CacheManager { - cache_path: PathBuf, - index: Arc>, + cache_dir: PathBuf, + /// Per-algorithm indexes + indexes: Arc>>, + /// Track which algorithms have been loaded + loaded: Arc>>, } impl CacheManager { pub fn new(cache_dir: &str) -> Self { - let cache_path = PathBuf::from(cache_dir).join(CACHE_FILENAME); Self { - cache_path, - index: Arc::new(RwLock::new(CacheIndex::default())), + cache_dir: PathBuf::from(cache_dir), + indexes: Arc::new(RwLock::new(HashMap::new())), + loaded: Arc::new(RwLock::new(HashSet::new())), } } - /// Load cache from disk + /// Load all indexes (called at startup) pub async fn load(&self) -> Result<()> { - if !self.cache_path.exists() { - tracing::info!("No existing cache found at {:?}", self.cache_path); + // Load indexes for all known algorithms + for algo in HashAlgorithm::all() { + self.ensure_loaded(algo.key()).await?; + } + Ok(()) + } + + /// Ensure a specific algorithm's index is loaded + async fn ensure_loaded(&self, algorithm: &str) -> Result<()> { + let mut loaded = self.loaded.write().await; + if loaded.contains(algorithm) { return Ok(()); } - let path = self.cache_path.clone(); - let index = tokio::task::spawn_blocking(move || -> Result { - let file = File::open(&path).context("Failed to open cache file")?; - let reader = BufReader::new(file); - let index: CacheIndex = - bincode::deserialize_from(reader).context("Failed to deserialize cache")?; + let mut indexes = self.indexes.write().await; + let mut index = AlgorithmIndex::new(&self.cache_dir, algorithm); - // Version check - if index.version != CACHE_VERSION { - tracing::warn!( - "Cache version mismatch (got {}, expected {}), starting fresh", - index.version, - CACHE_VERSION - ); - return Ok(CacheIndex::default()); - } + if let Err(e) = index.load() { + tracing::warn!("Failed to load index for {}: {}", algorithm, e); + } else { + tracing::info!("Loaded {} entries for {}", index.entries.len(), algorithm); + } - Ok(index) - }) - .await??; - - tracing::info!("Loaded {} cache entries", index.entries.len()); - *self.index.write().await = index; - Ok(()) - } - - /// Save cache to disk atomically - pub async fn save(&self) -> Result<()> { - let index = self.index.read().await.clone(); - let cache_path = self.cache_path.clone(); - - tokio::task::spawn_blocking(move || -> Result<()> { - // Ensure parent directory exists - if let Some(parent) = cache_path.parent() { - fs::create_dir_all(parent)?; - } - - // Write to temp file first - let temp_path = cache_path.with_extension("tmp"); - { - let file = File::create(&temp_path)?; - let writer = BufWriter::new(file); - bincode::serialize_into(writer, &index)?; - } - - // Atomic rename - fs::rename(&temp_path, &cache_path)?; - Ok(()) - }) - .await??; + indexes.insert(algorithm.to_string(), index); + loaded.insert(algorithm.to_string()); Ok(()) } - /// Get cached hashes for a file + /// Get cached hashes for a file (returns all available hashes) pub async fn get(&self, filename: &str, size: u64, mtime: i64) -> Option> { - let index = self.index.read().await; - index.get(filename, size, mtime).map(|e| e.hashes.clone()) + let indexes = self.indexes.read().await; + let mut results = HashMap::new(); + + for (algo, index) in indexes.iter() { + if let Some(hash) = index.get(filename, size, mtime) { + results.insert(algo.clone(), hash.clone()); + } + } + + if results.is_empty() { + None + } else { + Some(results) + } } - /// Store hashes in cache + /// Store hashes in cache (queues for append) pub async fn insert(&self, filename: String, size: u64, mtime: i64, hashes: HashMap) { - let mut index = self.index.write().await; - index.insert(filename, size, mtime, hashes); + let mut indexes = self.indexes.write().await; + + for (algo, hash) in hashes { + // Create index if it doesn't exist + if !indexes.contains_key(&algo) { + let mut index = AlgorithmIndex::new(&self.cache_dir, &algo); + let _ = index.load(); // Ignore errors, will create new file + indexes.insert(algo.clone(), index); + } + + if let Some(index) = indexes.get_mut(&algo) { + index.insert(filename.clone(), size, mtime, hash); + } + } } - /// Get missing hash algorithms for a file (not in cache or not computed) - pub async fn get_missing_algos( - &self, - filename: &str, - size: u64, - mtime: i64, - required: &[&str], - ) -> Vec { - let index = self.index.read().await; - let cached = index.get(filename, size, mtime); + /// Flush all pending writes to disk (MUST be called before reporting done) + pub async fn save(&self) -> Result<()> { + let mut indexes = self.indexes.write().await; + let mut total_written = 0; - required - .iter() - .filter(|algo| { - cached - .map(|e| !e.hashes.contains_key(**algo)) - .unwrap_or(true) - }) - .map(|s| s.to_string()) - .collect() + for (algo, index) in indexes.iter_mut() { + let pending = index.pending_writes.len(); + if pending > 0 { + index.flush().with_context(|| format!("Failed to flush index for {}", algo))?; + total_written += pending; + } + } + + if total_written > 0 { + tracing::info!("Flushed {} total cache entries to disk", total_written); + } + + Ok(()) } } @@ -176,25 +260,74 @@ mod tests { use tempfile::TempDir; #[tokio::test] - async fn test_cache_roundtrip() { + async fn test_cache_append_mode() { let temp_dir = TempDir::new().unwrap(); let cache = CacheManager::new(temp_dir.path().to_str().unwrap()); - // Insert some data - let mut hashes = HashMap::new(); - hashes.insert("cid_sha2-256".to_string(), "baejbeitest".to_string()); - cache - .insert("test.mkv".to_string(), 1000, 12345, hashes.clone()) - .await; - - // Save and reload + // Insert first entry + let mut hashes1 = HashMap::new(); + hashes1.insert("cid_sha2-256".to_string(), "baejbeitest1".to_string()); + cache.insert("test1.mkv".to_string(), 1000, 12345, hashes1).await; cache.save().await.unwrap(); - let cache2 = CacheManager::new(temp_dir.path().to_str().unwrap()); - cache2.load().await.unwrap(); + // Insert second entry + let mut hashes2 = HashMap::new(); + hashes2.insert("cid_sha2-256".to_string(), "baejbeitest2".to_string()); + cache.insert("test2.mkv".to_string(), 2000, 12346, hashes2).await; + cache.save().await.unwrap(); - let result = cache2.get("test.mkv", 1000, 12345).await; - assert!(result.is_some()); - assert_eq!(result.unwrap().get("cid_sha2-256").unwrap(), "baejbeitest"); + // Verify file contents (should have header + 2 entries) + let csv_path = temp_dir.path().join("index-cid_sha2-256.csv"); + let content = fs::read_to_string(&csv_path).unwrap(); + let lines: Vec<&str> = content.lines().collect(); + + assert_eq!(lines.len(), 3); // header + 2 entries + assert!(lines[0].starts_with("path,")); + assert!(lines[1].contains("test1.mkv")); + assert!(lines[2].contains("test2.mkv")); + } + + #[tokio::test] + async fn test_cache_reload() { + let temp_dir = TempDir::new().unwrap(); + + // First cache instance + { + let cache = CacheManager::new(temp_dir.path().to_str().unwrap()); + let mut hashes = HashMap::new(); + hashes.insert("cid_sha2-256".to_string(), "baejbeitest".to_string()); + cache.insert("test.mkv".to_string(), 1000, 12345, hashes).await; + cache.save().await.unwrap(); + } + + // Second cache instance (reload) + { + let cache = CacheManager::new(temp_dir.path().to_str().unwrap()); + cache.load().await.unwrap(); + + let result = cache.get("test.mkv", 1000, 12345).await; + assert!(result.is_some()); + assert_eq!(result.unwrap().get("cid_sha2-256").unwrap(), "baejbeitest"); + } + } + + #[tokio::test] + async fn test_no_duplicate_writes() { + let temp_dir = TempDir::new().unwrap(); + let cache = CacheManager::new(temp_dir.path().to_str().unwrap()); + + // Insert same entry twice + let mut hashes = HashMap::new(); + hashes.insert("cid_sha2-256".to_string(), "baejbeitest".to_string()); + cache.insert("test.mkv".to_string(), 1000, 12345, hashes.clone()).await; + cache.insert("test.mkv".to_string(), 1000, 12345, hashes).await; + cache.save().await.unwrap(); + + // Verify only one entry in file + let csv_path = temp_dir.path().join("index-cid_sha2-256.csv"); + let content = fs::read_to_string(&csv_path).unwrap(); + let lines: Vec<&str> = content.lines().collect(); + + assert_eq!(lines.len(), 2); // header + 1 entry } } diff --git a/src/hasher/algorithms.rs b/src/hasher/algorithms.rs index 15313d8..8482b14 100644 --- a/src/hasher/algorithms.rs +++ b/src/hasher/algorithms.rs @@ -257,6 +257,137 @@ fn hash_file_single_pass( Ok((results, bt_leaf_hashes, file_size)) } +/// Hash data from bytes (for WebDAV streaming) +/// +/// Similar to hash_file but works with in-memory data. +/// Note: Midhash256 requires knowing file size and reading middle section, +/// so we compute it from the bytes directly. +pub fn hash_bytes( + data: &[u8], + filename: &str, + enabled: &HashSet, +) -> Result> { + let mut results = HashMap::new(); + let file_size = data.len() as u64; + + // Check what we need to compute + let needs_bt = enabled.contains(&HashAlgorithm::BtPiecesRoot) + || enabled.contains(&HashAlgorithm::BtInfoHash); + + // Initialize standard hashers + let mut sha256 = enabled.contains(&HashAlgorithm::Sha256).then(Sha256::new); + let mut sha1 = enabled.contains(&HashAlgorithm::Sha1).then(Sha1::new); + let mut md5 = enabled.contains(&HashAlgorithm::Md5).then(Md5::new); + let mut crc32 = enabled.contains(&HashAlgorithm::Crc32).then(crc32fast::Hasher::new); + let mut sha3_256 = enabled.contains(&HashAlgorithm::Sha3_256).then(Sha3_256::new); + let mut sha3_384 = enabled.contains(&HashAlgorithm::Sha3_384).then(Sha3_384::new); + + // Process data in chunks for BT leaf hashes + let mut bt_leaf_hashes: Vec<[u8; 32]> = Vec::new(); + + // Update all hashers with full data + if let Some(ref mut h) = sha256 { h.update(data); } + if let Some(ref mut h) = sha1 { h.update(data); } + if let Some(ref mut h) = md5 { h.update(data); } + if let Some(ref mut h) = crc32 { h.update(data); } + if let Some(ref mut h) = sha3_256 { h.update(data); } + if let Some(ref mut h) = sha3_384 { h.update(data); } + + // Compute BT leaf hashes + if needs_bt { + if file_size == 0 { + bt_leaf_hashes.push([0u8; 32]); + } else { + for chunk in data.chunks(BT_BLOCK_SIZE) { + let mut block = [0u8; BT_BLOCK_SIZE]; + block[..chunk.len()].copy_from_slice(chunk); + // Pad with zeros if needed (last block) + let mut hasher = Sha256::new(); + hasher.update(&block); + bt_leaf_hashes.push(hasher.finalize().into()); + } + } + + let pieces_root = compute_merkle_root(&bt_leaf_hashes); + + if enabled.contains(&HashAlgorithm::BtPiecesRoot) { + results.insert( + HashAlgorithm::BtPiecesRoot.key().to_string(), + to_cid(&pieces_root, HashAlgorithm::BtPiecesRoot), + ); + } + + if enabled.contains(&HashAlgorithm::BtInfoHash) { + match compute_info_hash(filename, file_size, &pieces_root) { + Ok(info_hash) => { + results.insert( + HashAlgorithm::BtInfoHash.key().to_string(), + to_cid(&info_hash, HashAlgorithm::BtInfoHash), + ); + } + Err(e) => { + tracing::warn!("Failed to compute BT info hash: {}", e); + } + } + } + } + + // Midhash256 from bytes + if enabled.contains(&HashAlgorithm::Midhash256) { + let hash = compute_midhash256_from_bytes(data); + results.insert( + HashAlgorithm::Midhash256.key().to_string(), + to_cid(&hash, HashAlgorithm::Midhash256), + ); + } + + // Finalize standard hashes + if let Some(h) = sha256 { + results.insert(HashAlgorithm::Sha256.key().to_string(), to_cid(&h.finalize(), HashAlgorithm::Sha256)); + } + if let Some(h) = sha1 { + results.insert(HashAlgorithm::Sha1.key().to_string(), to_cid(&h.finalize(), HashAlgorithm::Sha1)); + } + if let Some(h) = md5 { + results.insert(HashAlgorithm::Md5.key().to_string(), to_cid(&h.finalize(), HashAlgorithm::Md5)); + } + if let Some(h) = crc32 { + results.insert(HashAlgorithm::Crc32.key().to_string(), to_cid(&h.finalize().to_be_bytes(), HashAlgorithm::Crc32)); + } + if let Some(h) = sha3_256 { + results.insert(HashAlgorithm::Sha3_256.key().to_string(), to_cid(&h.finalize(), HashAlgorithm::Sha3_256)); + } + if let Some(h) = sha3_384 { + results.insert(HashAlgorithm::Sha3_384.key().to_string(), to_cid(&h.finalize(), HashAlgorithm::Sha3_384)); + } + + Ok(results) +} + +/// Compute midhash256 from bytes (same algorithm as file version) +fn compute_midhash256_from_bytes(data: &[u8]) -> [u8; 32] { + use sha2::Digest; + + const SAMPLE_SIZE: usize = 1024 * 1024; // 1MB + let file_size = data.len(); + + let mut hasher = Sha256::new(); + + // Always include file size + hasher.update(file_size.to_le_bytes()); + + if file_size <= SAMPLE_SIZE { + // Small file: hash entire content + hasher.update(data); + } else { + // Large file: hash middle 1MB + let middle_start = (file_size - SAMPLE_SIZE) / 2; + hasher.update(&data[middle_start..middle_start + SAMPLE_SIZE]); + } + + hasher.finalize().into() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/hasher/mod.rs b/src/hasher/mod.rs index be2747e..7a43265 100644 --- a/src/hasher/mod.rs +++ b/src/hasher/mod.rs @@ -3,7 +3,7 @@ pub mod btih_v2; mod cid; pub mod midhash256; -pub use algorithms::hash_file; +pub use algorithms::{hash_bytes, hash_file}; pub use btih_v2::{ compute_btv2_hashes, compute_info_hash, compute_merkle_root, format_magnet_link, format_magnet_urn, BtV2Hashes, BT_BLOCK_SIZE, diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index d35af3f..0000000 --- a/src/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * MetaMesh Plugin: full-hash - * - * ============================================================================ - * PLUGIN MOUNT ARCHITECTURE - DO NOT MODIFY WITHOUT AUTHORIZATION - * ============================================================================ - * - * Each plugin container has exactly 3 mounts (2 for plugins without output): - * - * 1. /files (READ-ONLY) - Shared media files, read access only - * 2. /cache (READ-WRITE) - Plugin-specific cache folder - * 3. /files/plugin/ (READ-WRITE) - Plugin output folder (if needed) - * - * This plugin (full-hash) only requires mounts 1 and 2: - * - /files (RO) - to read entire files for hashing - * - /cache (RW) - to cache computed hashes in CSV files - * - * Cache Structure (one CSV per hash type): - * - /cache/index-cid_sha2-256.csv - * - /cache/index-cid_sha1.csv - * - /cache/index-cid_md5.csv - * - /cache/index-cid_crc32.csv - * - /cache/index-cid_sha3-256.csv - * - /cache/index-cid_sha3-384.csv - * - * Cache key format: filename-size-mtime (recomputes if file changes) - * - * Configuration: - * - enabledHashes: comma-separated list of hash types to compute - * - Default: all available hashes - * - * SECURITY: Plugins must NEVER write to /files directly. - * All write operations go to /cache or /files/plugin/ only. - * - * ============================================================================ - */ - -import Fastify from 'fastify'; -import type { HealthResponse, ProcessRequest, ProcessResponse, CallbackPayload, ConfigureRequest, ConfigureResponse } from './types.js'; -import { manifest, process as processFile, configure } from './plugin.js'; - -const app = Fastify({ logger: true }); -let ready = false; - -app.get('/health', async (): Promise => ({ status: 'healthy', ready, version: manifest.version })); -app.get('/manifest', async () => manifest); -app.post<{ Body: ConfigureRequest }>('/configure', async (request): Promise => { - try { - configure(request.body.config); - console.log(`[${manifest.id}] Configuration updated`); - return { status: 'ok' }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[${manifest.id}] Configuration error:`, errorMessage); - return { status: 'error', error: errorMessage }; - } -}); -app.post<{ Body: ProcessRequest }>('/process', async (request, reply) => { - const { taskId, cid, filePath, callbackUrl, metaCoreUrl } = request.body; - if (!taskId || !cid || !filePath || !callbackUrl || !metaCoreUrl) { - return reply.send({ status: 'rejected', error: 'Missing required fields' } as ProcessResponse); - } - processFile(request.body, async (payload: CallbackPayload) => { - try { await fetch(callbackUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } - catch (error) { console.error(`[${manifest.id}] Callback error:`, error); } - }).catch(console.error); - return reply.send({ status: 'accepted' } as ProcessResponse); -}); - -const port = parseInt(process.env.PORT || '8080', 10); -app.listen({ port, host: '0.0.0.0' }).then(() => { ready = true; console.log(`[${manifest.id}] Listening on port ${port}`); }); -process.on('SIGTERM', async () => { ready = false; await app.close(); process.exit(0); }); diff --git a/src/lib.rs b/src/lib.rs index d6eb9db..f2c1912 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,4 @@ pub mod cache; pub mod client; pub mod config; pub mod hasher; +pub mod webdav; diff --git a/src/meta-core-client.ts b/src/meta-core-client.ts deleted file mode 100644 index edbe17c..0000000 --- a/src/meta-core-client.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * meta-core API Client for writing metadata - * Fails gracefully if meta-core is unavailable (logs warning, returns without throwing) - */ - -export class MetaCoreClient { - constructor(private baseUrl: string) {} - - private async safeFetch(url: string, options: RequestInit): Promise { - try { - return await fetch(url, { ...options, signal: AbortSignal.timeout(5000) }); - } catch (error) { - console.warn(`[MetaCoreClient] Warning: meta-core unavailable at ${this.baseUrl}`); - return null; - } - } - - async setProperty(hashId: string, key: string, value: string): Promise { - const response = await this.safeFetch(`${this.baseUrl}/meta/${hashId}/${key}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ value }), - }); - if (response && !response.ok) { - console.warn(`[MetaCoreClient] Failed to set property: ${response.status}`); - } - } - - async getProperty(hashId: string, key: string): Promise { - const response = await this.safeFetch(`${this.baseUrl}/meta/${hashId}/${key}`, { method: 'GET' }); - if (!response || response.status === 404) return null; - if (!response.ok) return null; - const data = await response.json() as { value?: string }; - return data.value ?? null; - } - - async mergeMetadata(hashId: string, metadata: Record): Promise { - const response = await this.safeFetch(`${this.baseUrl}/meta/${hashId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(metadata), - }); - if (response && !response.ok) { - console.warn(`[MetaCoreClient] Failed to merge metadata: ${response.status}`); - } - } - - async deleteProperty(hashId: string, key: string): Promise { - const response = await this.safeFetch(`${this.baseUrl}/meta/${hashId}/${key}`, { method: 'DELETE' }); - if (response && !response.ok && response.status !== 404) { - console.warn(`[MetaCoreClient] Failed to delete property: ${response.status}`); - } - } - - async addToSet(hashId: string, key: string, value: string): Promise { - const response = await this.safeFetch(`${this.baseUrl}/meta/${hashId}/_add/${key}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ value }), - }); - if (response && !response.ok) { - console.warn(`[MetaCoreClient] Failed to add to set: ${response.status}`); - } - } - - async getMetadata(hashId: string): Promise> { - const response = await this.safeFetch(`${this.baseUrl}/meta/${hashId}`, { method: 'GET' }); - if (!response || response.status === 404) return {}; - if (!response.ok) return {}; - const data = await response.json() as { metadata?: Record }; - return data.metadata ?? {}; - } -} diff --git a/src/plugin.ts b/src/plugin.ts deleted file mode 100644 index 1602181..0000000 --- a/src/plugin.ts +++ /dev/null @@ -1,510 +0,0 @@ -/** - * Full Hash Plugin - * - * Computes multiple hashes of the entire file with CSV-based caching. - * Uses the same CID format as the meta-hash library. - * - * Available hashes (all enabled by default): - * - cid_sha2-256: SHA-256 hash - * - cid_sha1: SHA-1 hash - * - cid_md5: MD5 hash - * - cid_crc32: CRC32 checksum - * - cid_sha3-256: SHA3-256 hash - * - cid_sha3-384: SHA3-384 hash - * - * Note: midhash256 is NOT computed here - it's computed in-process for fast file ID. - * Note: btih_v2 requires special handling (BitTorrent v2 merkle tree) - not implemented yet. - * - * Cache: - * - Stores computed hashes in CSV files: /cache/index-{hashType}.csv - * - Cache key is: filename-size-mtime (recomputes if file changes) - */ - -import { createReadStream, promises as fs } from 'fs'; -import { createHash, Hash } from 'crypto'; -import { stat } from 'fs/promises'; -import path from 'path'; -import { parse } from 'csv-parse'; -import { stringify } from 'csv-stringify/sync'; -import CRC32 from 'crc-32'; -import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js'; -import { MetaCoreClient } from './meta-core-client.js'; -import { createWebDAVClient, WebDAVClient } from './webdav-client.js'; - -// Initialize WebDAV client if WEBDAV_URL is set -const webdavClient = createWebDAVClient(); -if (webdavClient) { - console.log('[full-hash] Using WebDAV for file access'); -} else { - console.log('[full-hash] Using direct filesystem access'); -} - -// Cache folder path (mounted as /cache in container) -const CACHE_FOLDER_PATH = globalThis.process.env.CACHE_PATH || '/cache'; - -// Hash algorithm definitions -const HASH_ALGORITHMS = { - 'cid_sha2-256': { crypto: 'sha256', prefix: 'baejbei' }, - 'cid_sha1': { crypto: 'sha1', prefix: 'baeir' }, - 'cid_md5': { crypto: 'md5', prefix: 'bahkq' }, - 'cid_crc32': { crypto: null, prefix: 'bagzafmq' }, // Special handling - 'cid_sha3-256': { crypto: 'sha3-256', prefix: 'baelb' }, - 'cid_sha3-384': { crypto: 'sha3-384', prefix: 'baekr' }, -} as const; - -type HashAlgorithm = keyof typeof HASH_ALGORITHMS; - -// All available hash types (default: all enabled) -const ALL_HASH_TYPES: HashAlgorithm[] = Object.keys(HASH_ALGORITHMS) as HashAlgorithm[]; - -// Plugin configuration (set via /configure endpoint) -let enabledHashes: HashAlgorithm[] = [...ALL_HASH_TYPES]; - -// Per-hash-type caches -const hashCaches: Map> = new Map(); -const cacheLoaded: Set = new Set(); -const cacheChanges: Set = new Set(); - -/** - * Get index file path for a hash type - */ -function getIndexPath(hashType: HashAlgorithm): string { - return path.join(CACHE_FOLDER_PATH, `index-${hashType}.csv`); -} - -/** - * Load cache for a specific hash type - */ -async function loadHashCache(hashType: HashAlgorithm): Promise { - if (cacheLoaded.has(hashType)) return; - - try { - await fs.mkdir(CACHE_FOLDER_PATH, { recursive: true }); - - const indexPath = getIndexPath(hashType); - const exists = await fs.access(indexPath).then(() => true).catch(() => false); - - if (!exists) { - hashCaches.set(hashType, new Map()); - cacheLoaded.add(hashType); - return; - } - - const cache = new Map(); - const records: Array<{ path: string; size: string; mtime: string; hash: string }> = await new Promise((resolve, reject) => { - const results: Array<{ path: string; size: string; mtime: string; hash: string }> = []; - createReadStream(indexPath) - .pipe(parse({ columns: true, skip_empty_lines: true })) - .on('data', (record) => results.push(record)) - .on('end', () => resolve(results)) - .on('error', reject); - }); - - for (const record of records) { - const cacheKey = `${record.path}-${record.size}-${record.mtime}`; - cache.set(cacheKey, record.hash); - } - - hashCaches.set(hashType, cache); - cacheLoaded.add(hashType); - console.log(`[full-hash] Loaded ${cache.size} entries for ${hashType}`); - } catch (error) { - console.error(`[full-hash] Error loading cache for ${hashType}:`, error); - hashCaches.set(hashType, new Map()); - cacheLoaded.add(hashType); - } -} - -/** - * Save cache for a specific hash type - */ -async function saveHashCache(hashType: HashAlgorithm): Promise { - if (!cacheChanges.has(hashType)) return; - - try { - const cache = hashCaches.get(hashType); - if (!cache || cache.size === 0) return; - - const entries: Array<{ path: string; size: string; mtime: string; hash: string }> = []; - for (const [key, hash] of cache.entries()) { - const [filename, size, mtime] = key.split('-'); - // Handle filenames that might contain dashes - const parts = key.split('-'); - const mtimeIdx = parts.findIndex(p => p.includes('T') && p.includes('Z')); - if (mtimeIdx >= 2) { - const filename = parts.slice(0, mtimeIdx - 1).join('-'); - const size = parts[mtimeIdx - 1]; - const mtime = parts.slice(mtimeIdx).join('-'); - entries.push({ path: filename, size, mtime, hash }); - } - } - - const csvString = stringify(entries, { - header: true, - columns: ['path', 'size', 'mtime', 'hash'], - }); - - await fs.writeFile(getIndexPath(hashType), csvString); - cacheChanges.delete(hashType); - console.log(`[full-hash] Saved ${entries.length} entries for ${hashType}`); - } catch (error) { - console.error(`[full-hash] Error saving cache for ${hashType}:`, error); - } -} - -/** - * Get cached hash value - */ -function getCachedHash(hashType: HashAlgorithm, filename: string, size: number, mtime: string): string | null { - const cache = hashCaches.get(hashType); - if (!cache) return null; - const cacheKey = `${filename}-${size}-${mtime}`; - return cache.get(cacheKey) || null; -} - -/** - * Add hash to cache - */ -function addToCache(hashType: HashAlgorithm, filename: string, size: number, mtime: string, hash: string): void { - let cache = hashCaches.get(hashType); - if (!cache) { - cache = new Map(); - hashCaches.set(hashType, cache); - } - const cacheKey = `${filename}-${size}-${mtime}`; - cache.set(cacheKey, hash); - cacheChanges.add(hashType); -} - -/** - * Encode bytes to base32 (RFC 4648, lowercase, no padding) - */ -function base32Encode(buffer: Buffer): string { - const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; - let result = ''; - let bits = 0; - let value = 0; - - for (const byte of buffer) { - value = (value << 8) | byte; - bits += 8; - while (bits >= 5) { - bits -= 5; - result += alphabet[(value >> bits) & 0x1f]; - } - } - - if (bits > 0) { - result += alphabet[(value << (5 - bits)) & 0x1f]; - } - - return result; -} - -/** - * Format hash as CID with appropriate prefix - */ -function formatAsCid(hashType: HashAlgorithm, hashHex: string): string { - const { prefix } = HASH_ALGORITHMS[hashType]; - const hashBuffer = Buffer.from(hashHex, 'hex'); - const base32Hash = base32Encode(hashBuffer); - return `${prefix}${base32Hash}`; -} - -/** - * Compute all hashes for a file in a single pass (filesystem mode) - */ -async function computeHashesFromFilesystem(filePath: string, hashTypes: HashAlgorithm[]): Promise> { - return new Promise((resolve, reject) => { - const results = new Map(); - const hashers: Map = new Map(); - let crc32Value = 0; - const needsCrc32 = hashTypes.includes('cid_crc32'); - - // Initialize crypto hashers - for (const hashType of hashTypes) { - const algo = HASH_ALGORITHMS[hashType]; - if (algo.crypto) { - try { - hashers.set(hashType, createHash(algo.crypto)); - } catch (e) { - console.warn(`[full-hash] Hash algorithm ${algo.crypto} not available, skipping ${hashType}`); - } - } - } - - const stream = createReadStream(filePath); - - stream.on('data', (chunk: Buffer | string) => { - const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); - // Update all crypto hashers - for (const [, hasher] of hashers) { - hasher.update(data); - } - // Update CRC32 - if (needsCrc32) { - crc32Value = CRC32.buf(data, crc32Value); - } - }); - - stream.on('end', () => { - // Finalize crypto hashes - for (const [hashType, hasher] of hashers) { - const hexHash = hasher.digest('hex'); - const cid = formatAsCid(hashType, hexHash); - results.set(hashType, cid); - } - // Finalize CRC32 - if (needsCrc32) { - // Convert to unsigned 32-bit and then to hex - const unsigned = crc32Value >>> 0; - const hexHash = unsigned.toString(16).padStart(8, '0'); - const cid = formatAsCid('cid_crc32', hexHash); - results.set('cid_crc32', cid); - } - resolve(results); - }); - - stream.on('error', reject); - }); -} - -/** - * Compute all hashes for a file via WebDAV streaming - */ -async function computeHashesFromWebDAV(client: WebDAVClient, filePath: string, hashTypes: HashAlgorithm[]): Promise> { - const results = new Map(); - const hashers: Map = new Map(); - let crc32Value = 0; - const needsCrc32 = hashTypes.includes('cid_crc32'); - - // Initialize crypto hashers - for (const hashType of hashTypes) { - const algo = HASH_ALGORITHMS[hashType]; - if (algo.crypto) { - try { - hashers.set(hashType, createHash(algo.crypto)); - } catch (e) { - console.warn(`[full-hash] Hash algorithm ${algo.crypto} not available, skipping ${hashType}`); - } - } - } - - // Stream file from WebDAV - for await (const chunk of client.streamFile(filePath)) { - // Update all crypto hashers - for (const [, hasher] of hashers) { - hasher.update(chunk); - } - // Update CRC32 - if (needsCrc32) { - crc32Value = CRC32.buf(chunk, crc32Value); - } - } - - // Finalize crypto hashes - for (const [hashType, hasher] of hashers) { - const hexHash = hasher.digest('hex'); - const cid = formatAsCid(hashType, hexHash); - results.set(hashType, cid); - } - - // Finalize CRC32 - if (needsCrc32) { - const unsigned = crc32Value >>> 0; - const hexHash = unsigned.toString(16).padStart(8, '0'); - const cid = formatAsCid('cid_crc32', hexHash); - results.set('cid_crc32', cid); - } - - return results; -} - -/** - * Compute all hashes for a file in a single pass - */ -async function computeHashes(filePath: string, hashTypes: HashAlgorithm[]): Promise> { - if (webdavClient) { - return computeHashesFromWebDAV(webdavClient, filePath, hashTypes); - } - return computeHashesFromFilesystem(filePath, hashTypes); -} - -export const manifest: PluginManifest = { - id: 'full-hash', - name: 'Full File Hash', - version: '1.0.0', - description: 'Computes multiple hashes (SHA-256, SHA-1, MD5, CRC32, SHA3) with CSV caching', - author: 'MetaMesh', - dependencies: [], - priority: 100, - color: '#9C27B0', - defaultQueue: 'background', - timeout: 300000, // 5 minutes for large files - schema: { - 'cid_sha2-256': { - label: 'SHA-256 Hash', - type: 'cid', - readonly: true, - hint: 'SHA-256 hash in CID format', - }, - 'cid_sha1': { - label: 'SHA-1 Hash', - type: 'cid', - readonly: true, - hint: 'SHA-1 hash in CID format', - }, - 'cid_md5': { - label: 'MD5 Hash', - type: 'cid', - readonly: true, - hint: 'MD5 hash in CID format', - }, - 'cid_crc32': { - label: 'CRC32 Checksum', - type: 'cid', - readonly: true, - hint: 'CRC32 checksum in CID format', - }, - 'cid_sha3-256': { - label: 'SHA3-256 Hash', - type: 'cid', - readonly: true, - hint: 'SHA3-256 hash in CID format', - }, - 'cid_sha3-384': { - label: 'SHA3-384 Hash', - type: 'cid', - readonly: true, - hint: 'SHA3-384 hash in CID format', - }, - }, - config: { - enabledHashes: { - type: 'string', - label: 'Enabled hash algorithms (comma-separated)', - default: 'cid_sha2-256,cid_sha1,cid_md5,cid_crc32,cid_sha3-256,cid_sha3-384', - required: false, - }, - }, -}; - -/** - * Configure which hashes to compute - */ -export function configure(config: Record): void { - if (config.enabledHashes && typeof config.enabledHashes === 'string') { - const requested = config.enabledHashes.split(',').map(s => s.trim()) as HashAlgorithm[]; - enabledHashes = requested.filter(h => h in HASH_ALGORITHMS); - console.log(`[full-hash] Enabled hashes: ${enabledHashes.join(', ')}`); - } -} - -export async function process( - request: ProcessRequest, - sendCallback: (payload: CallbackPayload) => Promise -): Promise { - const startTime = Date.now(); - const metaCore = new MetaCoreClient(request.metaCoreUrl); - - try { - const { cid, filePath, existingMeta } = request; - - // Determine which hashes need to be computed - const hashesToCompute: HashAlgorithm[] = []; - const hashesFromCache: Map = new Map(); - - // Get file stats for cache lookup - let fileSize: number; - let mtime: string; - const filename = path.basename(filePath); - - if (webdavClient) { - const stats = await webdavClient.stat(filePath); - fileSize = stats.size; - mtime = stats.mtime?.toISOString() || new Date().toISOString(); - } else { - const stats = await stat(filePath); - fileSize = stats.size; - mtime = stats.mtime.toISOString(); - } - - // Load caches and check what's needed - for (const hashType of enabledHashes) { - // Skip if already in metadata - if (existingMeta?.[hashType]) { - continue; - } - - // Load cache for this hash type - await loadHashCache(hashType); - - // Check cache - const cachedHash = getCachedHash(hashType, filename, fileSize, mtime); - if (cachedHash) { - hashesFromCache.set(hashType, cachedHash); - } else { - hashesToCompute.push(hashType); - } - } - - // Skip if nothing to do - if (hashesToCompute.length === 0 && hashesFromCache.size === 0) { - console.log(`[full-hash] All hashes already computed for ${filePath}, skipping`); - await sendCallback({ - taskId: request.taskId, - status: 'skipped', - duration: Date.now() - startTime, - reason: 'All hashes already computed', - }); - return; - } - - // Compute missing hashes - let computedHashes = new Map(); - if (hashesToCompute.length > 0) { - console.log(`[full-hash] Computing ${hashesToCompute.length} hashes for ${filePath}`); - computedHashes = await computeHashes(filePath, hashesToCompute); - - // Add computed hashes to cache - for (const [hashType, hash] of computedHashes) { - addToCache(hashType, filename, fileSize, mtime, hash); - } - - // Save updated caches - for (const hashType of hashesToCompute) { - await saveHashCache(hashType); - } - } - - // Merge cached and computed hashes - const allHashes = new Map([...hashesFromCache, ...computedHashes]); - - // Store all hashes to meta-core - for (const [hashType, hash] of allHashes) { - await metaCore.setProperty(cid, hashType, hash); - } - - const cacheHits = hashesFromCache.size; - const computed = computedHashes.size; - const mode = webdavClient ? 'WebDAV' : 'filesystem'; - console.log(`[full-hash] Stored ${allHashes.size} hashes (${cacheHits} cached, ${computed} computed, ${mode})`); - - await sendCallback({ - taskId: request.taskId, - status: 'completed', - duration: Date.now() - startTime, - }); - } catch (error) { - const duration = Date.now() - startTime; - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[full-hash] Error computing hashes for ${request.filePath}:`, errorMessage); - - await sendCallback({ - taskId: request.taskId, - status: 'failed', - duration, - error: errorMessage, - }); - } -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 4dbbfe2..0000000 --- a/src/types.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * MetaMesh Plugin Types - */ - -export interface PluginManifest { - id: string; - name: string; - version: string; - description?: string; - author?: string; - dependencies?: string[]; - schema?: Record; - config?: Record; - priority?: number; - color?: string; - defaultQueue?: 'fast' | 'background'; - timeout?: number; -} - -export interface SchemaField { - label: string; - type?: 'string' | 'number' | 'boolean' | 'select' | 'text' | 'array' | 'json' | 'cid'; - readonly?: boolean; - hint?: string; -} - -export interface ConfigField { - type: 'string' | 'number' | 'boolean' | 'select'; - label?: string; - required?: boolean; - default?: unknown; - secret?: boolean; -} - -export interface HealthResponse { - status: 'healthy' | 'unhealthy'; - ready: boolean; - version: string; -} - -export interface ProcessRequest { - taskId: string; - cid: string; - filePath: string; - callbackUrl: string; - metaCoreUrl: string; - existingMeta?: Record; -} - -export interface ProcessResponse { - status: 'accepted' | 'rejected'; - error?: string; -} - -export interface CallbackPayload { - taskId: string; - status: 'completed' | 'failed' | 'skipped'; - duration: number; - error?: string; - reason?: string; -} - -export interface ConfigureRequest { - config: Record; -} - -export interface ConfigureResponse { - status: 'ok' | 'error'; - error?: string; -} diff --git a/src/webdav-client.ts b/src/webdav-client.ts deleted file mode 100644 index 0b1609a..0000000 --- a/src/webdav-client.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * WebDAV Client for Plugin File Access - * - * Provides HTTP-based file access to meta-sort's WebDAV endpoint. - * Replaces direct filesystem access for containerized plugins. - */ - -export interface FileStats { - size: number; - mtime?: Date; -} - -export class WebDAVClient { - private baseUrl: string; - - constructor(baseUrl: string) { - // Remove trailing slash if present - this.baseUrl = baseUrl.replace(/\/$/, ''); - } - - /** - * Convert absolute file path to WebDAV URL - * Strips /files prefix and appends to base URL - */ - toWebDAVUrl(filePath: string): string { - // filePath is like "/files/watch/movie.mkv" - // We need to convert to "http://meta-sort-dev/webdav/watch/movie.mkv" - let relativePath = filePath; - - // Strip /files prefix if present - if (relativePath.startsWith('/files/')) { - relativePath = relativePath.substring(6); // Remove "/files" - } else if (relativePath.startsWith('/files')) { - relativePath = relativePath.substring(6); - } - - // Ensure path starts with / - if (!relativePath.startsWith('/')) { - relativePath = '/' + relativePath; - } - - return this.baseUrl + relativePath; - } - - /** - * Get file stats (size, mtime) via HTTP HEAD request - */ - async stat(filePath: string): Promise { - const url = this.toWebDAVUrl(filePath); - - const response = await fetch(url, { method: 'HEAD' }); - - if (!response.ok) { - throw new Error(`WebDAV HEAD failed for ${filePath}: ${response.status} ${response.statusText}`); - } - - const contentLength = response.headers.get('content-length'); - const lastModified = response.headers.get('last-modified'); - - return { - size: contentLength ? parseInt(contentLength, 10) : 0, - mtime: lastModified ? new Date(lastModified) : undefined, - }; - } - - /** - * Stream file content for hashing (returns async iterator of chunks) - */ - async *streamFile(filePath: string): AsyncGenerator { - const url = this.toWebDAVUrl(filePath); - - const response = await fetch(url, { method: 'GET' }); - - if (!response.ok) { - throw new Error(`WebDAV GET failed for ${filePath}: ${response.status} ${response.statusText}`); - } - - if (!response.body) { - throw new Error(`WebDAV GET returned no body for ${filePath}`); - } - - const reader = response.body.getReader(); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - yield Buffer.from(value); - } - } finally { - reader.releaseLock(); - } - } - - /** - * Read first N bytes of a file (for magic byte detection) - * Uses HTTP Range request - */ - async readBytes(filePath: string, start: number, end: number): Promise { - const url = this.toWebDAVUrl(filePath); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Range': `bytes=${start}-${end}`, - }, - }); - - if (!response.ok && response.status !== 206) { - throw new Error(`WebDAV Range GET failed for ${filePath}: ${response.status} ${response.statusText}`); - } - - const arrayBuffer = await response.arrayBuffer(); - return Buffer.from(arrayBuffer); - } - - /** - * Read entire file as buffer (for small files only) - */ - async readFile(filePath: string): Promise { - const url = this.toWebDAVUrl(filePath); - - const response = await fetch(url, { method: 'GET' }); - - if (!response.ok) { - throw new Error(`WebDAV GET failed for ${filePath}: ${response.status} ${response.statusText}`); - } - - const arrayBuffer = await response.arrayBuffer(); - return Buffer.from(arrayBuffer); - } - - /** - * Write file via HTTP PUT - */ - async writeFile(filePath: string, data: Buffer | string): Promise { - const url = this.toWebDAVUrl(filePath); - - const response = await fetch(url, { - method: 'PUT', - body: data, - headers: { - 'Content-Type': 'application/octet-stream', - }, - }); - - if (!response.ok) { - throw new Error(`WebDAV PUT failed for ${filePath}: ${response.status} ${response.statusText}`); - } - } - - /** - * Check if file exists via HTTP HEAD - */ - async exists(filePath: string): Promise { - const url = this.toWebDAVUrl(filePath); - - try { - const response = await fetch(url, { method: 'HEAD' }); - return response.ok; - } catch { - return false; - } - } -} - -/** - * Create a WebDAV client from environment variables - */ -export function createWebDAVClient(): WebDAVClient | null { - const webdavUrl = process.env.WEBDAV_URL; - - if (!webdavUrl) { - console.warn('[webdav-client] WEBDAV_URL not set, WebDAV client unavailable'); - return null; - } - - return new WebDAVClient(webdavUrl); -} diff --git a/src/webdav.rs b/src/webdav.rs new file mode 100644 index 0000000..75796cb --- /dev/null +++ b/src/webdav.rs @@ -0,0 +1,114 @@ +use anyhow::{Context, Result}; +use reqwest::Client; + +/// WebDAV client for streaming file access +pub struct WebDavClient { + client: Client, + base_url: String, +} + +impl WebDavClient { + pub fn new(base_url: &str) -> Self { + Self { + client: Client::new(), + base_url: base_url.trim_end_matches('/').to_string(), + } + } + + /// Build full URL for a file path + /// The input path is a filesystem path like /files/watch/movie.mkv + /// The WebDAV server exposes /files as /webdav, so we strip /files prefix + fn file_url(&self, path: &str) -> String { + // Strip /files prefix since WebDAV mounts /files at the base URL + let clean_path = path + .trim_start_matches('/') + .strip_prefix("files/") + .unwrap_or(path.trim_start_matches('/')); + format!("{}/{}", self.base_url, clean_path) + } + + /// Stream file contents via HTTP GET + pub async fn get_file(&self, path: &str) -> Result { + let url = self.file_url(path); + + let response = self.client + .get(&url) + .send() + .await + .with_context(|| format!("Failed to GET {}", url))?; + + if !response.status().is_success() { + anyhow::bail!("WebDAV GET failed: {} for {}", response.status(), url); + } + + let content_length = response.content_length(); + + Ok(WebDavReader { + inner: response, + size: content_length, + }) + } + + /// Get file size without downloading content (HEAD request) + pub async fn get_size(&self, path: &str) -> Result { + let url = self.file_url(path); + + let response = self.client + .head(&url) + .send() + .await + .with_context(|| format!("Failed to HEAD {}", url))?; + + if !response.status().is_success() { + anyhow::bail!("WebDAV HEAD failed: {} for {}", response.status(), url); + } + + response + .content_length() + .ok_or_else(|| anyhow::anyhow!("No Content-Length header for {}", url)) + } +} + +/// Async reader wrapper for WebDAV response +pub struct WebDavReader { + inner: reqwest::Response, + size: Option, +} + +impl WebDavReader { + pub fn size(&self) -> Option { + self.size + } + + /// Read all bytes (for smaller files or when you need the whole thing) + pub async fn bytes(self) -> Result> { + self.inner.bytes().await + .map(|b| b.to_vec()) + .context("Failed to read response bytes") + } + + /// Get the underlying response for streaming + pub fn into_response(self) -> reqwest::Response { + self.inner + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_file_url() { + let client = WebDavClient::new("http://localhost/webdav"); + // /files prefix should be stripped + assert_eq!(client.file_url("/files/watch/test.mkv"), "http://localhost/webdav/watch/test.mkv"); + assert_eq!(client.file_url("files/watch/test.mkv"), "http://localhost/webdav/watch/test.mkv"); + + // Trailing slash in base URL should be handled + let client2 = WebDavClient::new("http://localhost/webdav/"); + assert_eq!(client2.file_url("/files/watch/test.mkv"), "http://localhost/webdav/watch/test.mkv"); + + // Paths without /files prefix should work too + assert_eq!(client.file_url("watch/test.mkv"), "http://localhost/webdav/watch/test.mkv"); + } +} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 95ac5c2..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -}