diff --git a/Cargo.lock b/Cargo.lock index 036e2b9..dedfc6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -34,9 +34,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -52,14 +52,21 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "asn1-rs" version = "0.7.1" @@ -99,12 +106,33 @@ dependencies = [ "syn", ] +[[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", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic-write-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84790c55b5704b0d35130bf16a4ce22a8e70eb0ea773522557524d9a4852663d" +dependencies = [ + "nix", + "rand", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -113,9 +141,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "zeroize", @@ -123,9 +151,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -211,9 +239,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -221,9 +249,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -233,9 +261,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -251,9 +279,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "cliclack" -version = "0.3.9" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4797110534d49f4e38465be8d84c911f3a9e0f6582f70d3aa4cb30c8fa737851" +checksum = "e00298c57c88d24e4491d4f2153c9afd91076865ab38f274a709d5c5f7e112aa" dependencies = [ "console", "indicatif", @@ -418,6 +446,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -429,6 +478,24 @@ dependencies = [ "syn", ] +[[package]] +name = "dlmgr" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf1a6acc5f81ef27c27a6d3d68d1f52af02ad0f8d2def0024c1a64540a03317" +dependencies = [ + "anyhow", + "async-trait", + "atomic-write-file", + "hex", + "reqwest", + "sha2", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", +] + [[package]] name = "duct" version = "1.1.1" @@ -498,9 +565,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -573,8 +640,11 @@ dependencies = [ "cliclack", "derive_more", "dialoguer", + "directories", + "dlmgr", "duct", "futures-util", + "hex-literal", "home", "humansize", "pem", @@ -591,6 +661,7 @@ dependencies = [ "tokio-tungstenite", "unicode-segmentation", "url", + "which", "x509-parser", ] @@ -662,6 +733,18 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + [[package]] name = "home" version = "0.5.12" @@ -806,14 +889,106 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "idna" -version = "0.5.0" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", ] [[package]] @@ -927,12 +1102,27 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "log" version = "0.4.21" @@ -974,6 +1164,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1033,12 +1235,24 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_pipe" version = "1.2.3" @@ -1061,9 +1275,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1077,6 +1291,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1091,9 +1314,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1128,9 +1351,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -1164,9 +1387,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1220,6 +1443,17 @@ dependencies = [ "yasna", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.17", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -1513,6 +1747,17 @@ dependencies = [ "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 = "shared_child" version = "1.1.1" @@ -1624,6 +1869,12 @@ dependencies = [ "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 = "strsim" version = "0.11.1" @@ -1638,9 +1889,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1690,9 +1941,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -1783,6 +2034,16 @@ dependencies = [ "time-core", ] +[[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.6.0" @@ -1910,19 +2171,31 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-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", +] + [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -1958,12 +2231,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1976,15 +2243,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -2011,13 +2269,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -2026,11 +2285,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" @@ -2190,6 +2455,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +dependencies = [ + "libc", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2521,6 +2795,12 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "x509-parser" version = "0.18.1" @@ -2548,6 +2828,50 @@ dependencies = [ "time", ] +[[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", + "synstructure", +] + +[[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", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" @@ -2568,6 +2892,39 @@ dependencies = [ "syn", ] +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 533d4dc..689ee76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,16 @@ edition = "2024" [dependencies] base64 = "0.22.1" -clap = { version = "4.5.60", features = ["cargo", "derive"] } +clap = { version = "4.6", features = ["cargo", "derive"] } +cliclack = "0.4.1" +chrono = "0.4.44" derive_more = { version = "2.1.1", features = ["constructor"] } dialoguer = { version = "0.12.0" } +directories = "6.0.0" +dlmgr = "0.3.1" duct = { version = "1.1.1" } futures-util = { version = "0.3" } +hex-literal = "1.1.0" home = { version = "0.5.12" } humansize = "2" qemu_img_cmd_types = { path = "libs/qemu_img_cmd_types" } @@ -28,15 +33,14 @@ rustls = "0.23.37" serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.149" } snafu = { version = "0.9.0", features = ["rust_1_81"] } -tempfile = "3.26.0" +tempfile = "3.27.0" time = "0.3.47" tokio = { version = "1.50.0", features = ["rt", "macros", "io-std", "fs"] } tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots", "stream", "connect"], default-features = false } url = { version = "2.5.0" } x509-parser = "0.18.1" -cliclack = "0.3.9" unicode-segmentation = "1.12.0" -chrono = "0.4.44" +which = "8.0.2" [build-dependencies] diff --git a/libs/qemu_img_cmd_types/src/info/types.rs b/libs/qemu_img_cmd_types/src/info/types.rs index 2d85c48..6f01b02 100644 --- a/libs/qemu_img_cmd_types/src/info/types.rs +++ b/libs/qemu_img_cmd_types/src/info/types.rs @@ -11,7 +11,7 @@ pub struct QemuInfo { #[serde(rename = "actual-size")] pub actual_size: u64, #[serde(rename = "dirty-flag")] - pub dirty_flag: bool, + pub dirty_flag: Option, pub children: Vec>, #[serde(rename = "format-specific", skip_serializing_if = "Option::is_none")] pub format_specific: Option, diff --git a/src/args.rs b/src/args.rs index 0add592..ec2258d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -24,6 +24,8 @@ pub struct GlobalArguments { #[derive(clap::Subcommand)] pub enum Action { + #[clap(hide = true)] + DlQemuImg, #[clap(hide = true)] Proxy(crate::tasks_internal::proxy::ProxyArguments), diff --git a/src/helpers/helper_cmd_error.rs b/src/helpers/helper_cmd_error.rs index 3515a72..cb772cd 100644 --- a/src/helpers/helper_cmd_error.rs +++ b/src/helpers/helper_cmd_error.rs @@ -1,15 +1,23 @@ use snafu::prelude::*; #[derive(Debug, Snafu)] +#[snafu(visibility(pub))] pub enum HelperCommandError { - #[snafu(transparent)] + #[snafu(display("JSON Serialization/Deserialization error"), context(false))] JsonError { source: serde_json::Error }, - #[snafu(transparent)] + #[snafu(display("IO Error"), context(false))] IoError { source: std::io::Error }, - #[snafu(transparent)] + #[snafu(display("IO Error {action}"))] + IoErrorPerformingAction { + source: std::io::Error, + action: &'static str, + }, + #[snafu(display("Task Panicked"), context(false))] TaskPanicked { source: tokio::task::JoinError }, #[snafu(display("Helper command returned invalid response: {reason}"))] InvalidResponse { reason: &'static str }, + #[snafu(display("qemu-img not found"))] + QemuImgNotFound, #[snafu(whatever, display("{message}"))] UnhandledError { message: String, diff --git a/src/helpers/mtls/mod.rs b/src/helpers/mtls/mod.rs index 6793ee8..c063a6d 100644 --- a/src/helpers/mtls/mod.rs +++ b/src/helpers/mtls/mod.rs @@ -3,6 +3,7 @@ use crate::api::storage_api::entities::CmdSubmitResponse; use crate::helpers::cmd::cmd_response::poll_for_cmd_response_type; use crate::helpers::helper_cmd_error::HelperCommandError; use crate::helpers::mtls::init_mtls_cmd::InitMtlsCredentialsCmdResponse; +use crate::task_common::error::HelperCommandSnafu; use crate::task_common::error::TaskError; use base64::Engine; use base64::engine::GeneralPurpose; @@ -57,7 +58,9 @@ impl MtlsCredentialHelper { let init_response: InitMtlsCredentialsCmdResponse = poll_for_cmd_response_type(cmd_api, submit_resp, "INIT_MTLS_CREDENTIALS").await?; - Ok(MtlsIssuedCredentialHelper::build(self.keypair, init_response).await?) + MtlsIssuedCredentialHelper::build(self.keypair, init_response) + .await + .context(HelperCommandSnafu) } } diff --git a/src/helpers/qemu/mod.rs b/src/helpers/qemu/mod.rs index 4f9a87a..4c18ddd 100644 --- a/src/helpers/qemu/mod.rs +++ b/src/helpers/qemu/mod.rs @@ -1,7 +1,9 @@ mod convert_progress; +pub mod qemu_img_cmd_provider; use crate::helpers::helper_cmd_error::HelperCommandError; use crate::helpers::qemu::convert_progress::{QemuConvertProgressProvider, report_progress}; +use crate::helpers::qemu::qemu_img_cmd_provider::QemuImgCmdProvider; use duct::Expression; use qemu_img_cmd_types::info::QemuInfo; use std::path::{Path, PathBuf}; @@ -9,12 +11,21 @@ use std::process::Output; use std::sync::Arc; use tokio::task::{JoinError, JoinHandle}; -pub async fn qemu_img_info(path: &Path) -> Result { +pub async fn qemu_img_info( + qemu_img: QemuImgCmdProvider, + path: &Path, +) -> Result { let path_as_os_str = path.as_os_str().to_os_string(); let qemu_info_json = tokio::task::spawn_blocking(move || { - duct::cmd!("qemu-img", "info", "--output", "json", &path_as_os_str) - .stdout_capture() - .read() + duct::cmd!( + qemu_img.bin_path, + "info", + "--output", + "json", + &path_as_os_str + ) + .stdout_capture() + .read() }) .await??; @@ -70,14 +81,14 @@ impl QemuImgConvert { ) } - fn build_expression(&self) -> Expression { + fn build_expression(&self, qemu_img: QemuImgCmdProvider) -> Expression { match self.op { ConvertOperation::Import { ref source_file, ref source_format, } => { duct::cmd!( - "qemu-img", + &qemu_img.bin_path, "convert", "-p", //Display progress bar "-n", //Skip the creation of the target volume @@ -95,7 +106,7 @@ impl QemuImgConvert { ref target_format, } => { duct::cmd!( - "qemu-img", + &qemu_img.bin_path, "convert", "-p", //Display progress bar "--object", @@ -112,6 +123,7 @@ impl QemuImgConvert { } pub async fn qemu_img_convert( + qemu_img: QemuImgCmdProvider, args: QemuImgConvert, ) -> ( Arc, @@ -120,7 +132,7 @@ pub async fn qemu_img_convert( let convert_progress_provider = Arc::new(QemuConvertProgressProvider::default()); let convert_progress_provider2 = convert_progress_provider.clone(); let task_handle = tokio::task::spawn_blocking(move || { - let reader = args.build_expression().reader()?; + let reader = args.build_expression(qemu_img).reader()?; report_progress(convert_progress_provider2, reader) }); diff --git a/src/helpers/qemu/qemu_img_cmd_provider/dl_win.rs b/src/helpers/qemu/qemu_img_cmd_provider/dl_win.rs new file mode 100644 index 0000000..6d8c91d --- /dev/null +++ b/src/helpers/qemu/qemu_img_cmd_provider/dl_win.rs @@ -0,0 +1,93 @@ +use crate::helpers::helper_cmd_error::{HelperCommandError, IoErrorPerformingActionSnafu}; +use cliclack::progress_bar; +use directories::BaseDirs; +use dlmgr::consumers::atomic_file_consumer_sha256::AtomicFileConsumerSha256; +use dlmgr::{DownloadTask, DownloadTaskBuilder}; +use hex_literal::hex; +use snafu::ResultExt; +use std::path::Path; +use std::path::PathBuf; +use tokio::fs; +use url::Url; + +const QEMU_IMG_WIN_URL: &str = + "https://cl1.gallium-cdn.com/utils/qemu/win64/v10.2.1-20260305/qemu-img.exe"; +const QEMU_IMG_WIN_SHA256: [u8; 32] = + hex!("cfae8f5bbced4bc8ea2dc1ca9581349a23c30fc55221b5d8465b982b590e6330"); + +pub fn cache_dir_qemu_img_exe_path() -> Result { + let base_dirs = BaseDirs::new().ok_or_else(|| HelperCommandError::InvalidResponse { + reason: "base directories not available", + })?; + let cache_dir = base_dirs.cache_dir(); + + let cli_cache_dir = cache_dir.join("Gallium-CLI"); + + let bin_dir = cli_cache_dir.join("bin"); + + Ok(bin_dir.join("qemu-img.exe")) +} + +async fn get_and_create_parent_dir(path: &Path) -> Result<(), HelperCommandError> { + let parent_dir = path + .parent() + .ok_or_else(|| HelperCommandError::InvalidResponse { + reason: "dir parent not available", + })?; + + fs::create_dir_all(parent_dir) + .await + .context(IoErrorPerformingActionSnafu { + action: "dir parent create", + })?; + + Ok(()) +} + +pub async fn download_qemu_img() -> Result<(), HelperCommandError> { + let task_builder = DownloadTaskBuilder::new(); + let qemu_img_exe_path = cache_dir_qemu_img_exe_path()?; + + get_and_create_parent_dir(&qemu_img_exe_path).await?; + + let (consumer, mut complete_notify) = + AtomicFileConsumerSha256::new(qemu_img_exe_path, QEMU_IMG_WIN_SHA256) + .await + .whatever_context::<_, HelperCommandError>("setup download")?; + + let task: DownloadTask = task_builder + .begin_download( + Url::parse(QEMU_IMG_WIN_URL) + .whatever_context::<_, HelperCommandError>("parse download url")? + .into(), + consumer, + ) + .await + .whatever_context::<_, HelperCommandError>("begin download")?; + + let p = task.progress_provider(); + + let mut ui_tick = tokio::time::interval(tokio::time::Duration::from_millis(100)); + + let progress = progress_bar(100); + + progress.start("Downloading qemu-img"); + + loop { + tokio::select! { + _ = ui_tick.tick() => { + progress.set_position(p.progress_percent() as u64); + } + r = &mut complete_notify => { + progress.set_position(100); + r.whatever_context::<_, HelperCommandError>("await completion message")? + .whatever_context::<_, HelperCommandError>("validate qemu-img")?; + break; + } + } + } + + progress.stop("Download complete"); + + Ok(()) +} diff --git a/src/helpers/qemu/qemu_img_cmd_provider/mod.rs b/src/helpers/qemu/qemu_img_cmd_provider/mod.rs new file mode 100644 index 0000000..315e10d --- /dev/null +++ b/src/helpers/qemu/qemu_img_cmd_provider/mod.rs @@ -0,0 +1,50 @@ +pub mod dl_win; + +use crate::helpers::helper_cmd_error::HelperCommandError; +use std::path::PathBuf; + +#[derive(Clone)] +pub struct QemuImgCmdProvider { + pub bin_path: PathBuf, +} + +impl QemuImgCmdProvider { + pub async fn find_bin() -> Result { + if let Ok(bin_path) = std::env::var("QEMU_IMG_BIN").map(PathBuf::from) { + return if tokio::fs::try_exists(&bin_path).await.unwrap_or(false) { + Ok(QemuImgCmdProvider { bin_path }) + } else { + Err(HelperCommandError::InvalidResponse { + reason: "QEMU_IMG_BIN env var is set but does not point to a file", + }) + }; + } + + if cfg!(target_os = "windows") { + find_in_cache().await + } else { + find_in_path().await + } + } +} + +async fn find_in_cache() -> Result { + let bin_path = dl_win::cache_dir_qemu_img_exe_path()?; + if tokio::fs::try_exists(&bin_path).await.unwrap_or(false) { + Ok(QemuImgCmdProvider { bin_path }) + } else { + Err(HelperCommandError::QemuImgNotFound) + } +} + +async fn find_in_path() -> Result { + use which::which; + match tokio::task::spawn_blocking(|| which("qemu-img")).await? { + Ok(bin_path) => Ok(QemuImgCmdProvider { bin_path }), + Err(which::Error::CannotFindBinaryPath) => Err(HelperCommandError::QemuImgNotFound), + Err(e) => Err(HelperCommandError::UnhandledError { + message: format!("{e}"), + source: Some(Box::new(e)), + }), + } +} diff --git a/src/main.rs b/src/main.rs index 0c51edc..b3ab587 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ async fn main() -> Result<(), TaskError> { let invocation = Invocation::parse(); match invocation.action { + Some(Action::DlQemuImg) => crate::tasks_internal::qemu_img::dl_qemu_img().await, Some(Action::Proxy(args)) => crate::tasks_internal::proxy::proxy(&args).await, Some(Action::Export(args)) => { crate::tasks::export::export_main(&invocation.gargs, args).await diff --git a/src/task_common/error.rs b/src/task_common/error.rs index f57ce31..cc2cd0d 100644 --- a/src/task_common/error.rs +++ b/src/task_common/error.rs @@ -3,6 +3,7 @@ use crate::helpers::helper_cmd_error::HelperCommandError; use snafu::prelude::*; #[derive(Debug, Snafu)] +#[snafu(visibility(pub))] pub enum TaskError { #[snafu(display("Missing or invalid input for {field}"))] UserInputInvalid { field: &'static str }, @@ -21,7 +22,7 @@ pub enum TaskError { }, #[snafu(display("Requested operation not supported ({op}): {reason}"))] RequestedOperationNotSupported { op: &'static str, reason: String }, - #[snafu(transparent)] + #[snafu(display("API Client Error"), context(false))] ApiClientError { source: ApiClientError }, #[snafu(display("API Response missing expected field: {field}"))] ApiResponseMissingField { field: &'static str }, @@ -32,7 +33,7 @@ pub enum TaskError { cmd_type: String, serde_err: Option, }, - #[snafu(transparent)] + #[snafu(display("Helper command error"))] HelperCommand { source: HelperCommandError }, #[snafu(display("Failed to initialize {name}"))] Initialize { diff --git a/src/tasks/export/mod.rs b/src/tasks/export/mod.rs index 7299a4e..298a38e 100644 --- a/src/tasks/export/mod.rs +++ b/src/tasks/export/mod.rs @@ -8,9 +8,13 @@ use crate::helpers::auth::get_login_response_for_saved_credentials; use crate::helpers::cmd::cmd_progress::CommandProgressUpdater; use crate::helpers::mtls::MtlsCredentialHelper; use crate::helpers::nbd::poll_for_nbd_response; +use crate::task_common::error::HelperCommandSnafu; +use snafu::ResultExt; + use crate::helpers::qemu::{ConvertOperation, QemuImgConvert, qemu_img_convert}; use crate::task_common::error::TaskError; use crate::tasks::export::format::ExportFormat; +use crate::tasks_internal::qemu_img::ensure_qemu_img; use cliclack::{multi_progress, progress_bar, spinner}; use std::path::PathBuf; use std::sync::Arc; @@ -53,6 +57,8 @@ async fn process( vol_name: String, exp_format: ExportFormat, ) -> Result<(), TaskError> { + let qemu_img = ensure_qemu_img().await.context(HelperCommandSnafu)?; + let storage_api = api_client.storage_api(); let export_filename = format!("{vol_name}_export{}", exp_format.as_ext()); @@ -62,7 +68,7 @@ async fn process( let pb = multi.add(progress_bar(10000)); let spinner_final = multi.add(spinner()); - let mtls_helper = MtlsCredentialHelper::new()?; + let mtls_helper = MtlsCredentialHelper::new().context(HelperCommandSnafu)?; let path_params = ExportNbdVolumePathParams { cluster_id: source, @@ -71,7 +77,7 @@ async fn process( }; let req = VolumeNbdExportRequest { - csr_base64: mtls_helper.get_csr_base64()?, + csr_base64: mtls_helper.get_csr_base64().context(HelperCommandSnafu)?, }; let submit_resp = storage_api.export_nbd_volume(&path_params, &req).await?; @@ -82,8 +88,14 @@ async fn process( .poll_for_credentials(&cmd_api, &submit_resp) .await?; - let nbd_tls_hostname = mtls_helper.read_server_cert_hostname()?; - let cert_dir = mtls_helper.write_credentials().await?.to_path_buf(); + let nbd_tls_hostname = mtls_helper + .read_server_cert_hostname() + .context(HelperCommandSnafu)?; + let cert_dir = mtls_helper + .write_credentials() + .await + .context(HelperCommandSnafu)? + .to_path_buf(); spinner_init.start("Waiting for deployment"); @@ -107,7 +119,7 @@ async fn process( //TODO: this is copy-pasted from import, it should be factored out. // (but, does it need the same logic around waiting for completion?) - let (progress, mut task) = qemu_img_convert(convert_cmd).await; + let (progress, mut task) = qemu_img_convert(qemu_img.clone(), convert_cmd).await; let mut ui_tick = tokio::time::interval(tokio::time::Duration::from_millis(100)); let mut backend_tick = tokio::time::interval(tokio::time::Duration::from_millis(5000)); @@ -141,7 +153,7 @@ async fn process( spinner_final.error("Export failed"); multi.stop(); - Err(e.into()) + Err(TaskError::HelperCommand { source: e }) } }; } diff --git a/src/tasks/import/mod.rs b/src/tasks/import/mod.rs index cb26c4d..f88e6f4 100644 --- a/src/tasks/import/mod.rs +++ b/src/tasks/import/mod.rs @@ -12,9 +12,12 @@ use crate::helpers::auth::get_login_response_for_saved_credentials; use crate::helpers::cmd::cmd_progress::CommandProgressUpdater; use crate::helpers::mtls::MtlsCredentialHelper; use crate::helpers::nbd::poll_for_nbd_response; +use crate::helpers::qemu::qemu_img_cmd_provider::QemuImgCmdProvider; use crate::helpers::qemu::{ConvertOperation, QemuImgConvert, qemu_img_convert}; +use crate::task_common::error::HelperCommandSnafu; use crate::tasks::import::disk_pool::{DiskPoolDetermination, determine_disk_pool}; use crate::tasks::import::param_helpers::{description, truncate_name}; +use crate::tasks_internal::qemu_img::ensure_qemu_img; use cliclack::{confirm, log, multi_progress, progress_bar, spinner}; use humansize::{BINARY, format_size}; use snafu::ResultExt; @@ -44,6 +47,8 @@ pub(crate) async fn import_main( global_args: &crate::args::GlobalArguments, args: ImportArguments, ) -> Result<(), TaskError> { + let qemu_img = ensure_qemu_img().await.context(HelperCommandSnafu)?; + let api_client = global_args.build_api_client()?.with_access_token( get_login_response_for_saved_credentials(global_args) .await? @@ -55,7 +60,7 @@ pub(crate) async fn import_main( let mut import_sources = vec![]; for source in args.source.iter() { - let ScanResult { sources, warnings } = scan_import_sources(source).await?; + let ScanResult { sources, warnings } = scan_import_sources(&qemu_img, source).await?; for warning in warnings { log::warning(&warning).whatever_context::<_, TaskError>("writing to terminal")?; } @@ -64,7 +69,7 @@ pub(crate) async fn import_main( if confirm_import(&import_sources, &disk_pool, &args)? { for source in import_sources { - process(api_client.clone(), &args, &disk_pool, source).await?; + process(&qemu_img, api_client.clone(), &args, &disk_pool, source).await?; } } @@ -121,6 +126,7 @@ fn confirm_import( } async fn process( + qemu_img: &QemuImgCmdProvider, api_client: Arc, import_args: &ImportArguments, disk_pool: &DiskPoolDetermination, @@ -135,7 +141,7 @@ async fn process( spinner_init.start("Preparing import"); - let mtls_helper = MtlsCredentialHelper::new()?; + let mtls_helper = MtlsCredentialHelper::new().context(HelperCommandSnafu)?; let path_params = ImportNbdVolumePathParams { cluster_id: import_args.target.clone(), @@ -143,7 +149,7 @@ async fn process( }; let req = VolumeNbdImportRequest { - csr_base64: mtls_helper.get_csr_base64()?, + csr_base64: mtls_helper.get_csr_base64().context(HelperCommandSnafu)?, volume_description: Some(description(&source.name_part)), volume_size_gb: source.virtual_size_gb_round_up()?, volume_storage_class: disk_pool.kube_name.clone(), @@ -153,7 +159,7 @@ async fn process( let submit_resp = storage_api.import_nbd_volume(&path_params, &req).await?; spinner_init.start("Waiting for volume"); - //TODO: Poll all the commands to provide more detailed status as import porgresses + //TODO: Poll all the commands to provide more detailed status as import progresses let cmd_api = api_client.command_api(); @@ -161,8 +167,14 @@ async fn process( .poll_for_credentials(&cmd_api, &submit_resp) .await?; - let nbd_tls_hostname = mtls_helper.read_server_cert_hostname()?; - let cert_dir = mtls_helper.write_credentials().await?.to_path_buf(); + let nbd_tls_hostname = mtls_helper + .read_server_cert_hostname() + .context(HelperCommandSnafu)?; + let cert_dir = mtls_helper + .write_credentials() + .await + .context(HelperCommandSnafu)? + .to_path_buf(); spinner_init.start("Waiting for deployment"); @@ -184,7 +196,7 @@ async fn process( let progress_updater = CommandProgressUpdater::build_and_spawn(cmd_api, &submit_resp, "AWAIT_NBD_COMPLETION")?; - let (progress, mut task) = qemu_img_convert(convert_cmd).await; + let (progress, mut task) = qemu_img_convert(qemu_img.clone(), convert_cmd).await; let mut ui_tick = tokio::time::interval(tokio::time::Duration::from_millis(100)); let mut backend_tick = tokio::time::interval(tokio::time::Duration::from_millis(5000)); @@ -223,7 +235,7 @@ async fn process( spinner_final.error("Import failed"); multi.stop(); - Err(e.into()) + Err(TaskError::HelperCommand { source: e }) } }; } diff --git a/src/tasks/import/source_scan.rs b/src/tasks/import/source_scan.rs index 3c28619..0220d79 100644 --- a/src/tasks/import/source_scan.rs +++ b/src/tasks/import/source_scan.rs @@ -1,9 +1,9 @@ +use crate::helpers::qemu::qemu_img_cmd_provider::QemuImgCmdProvider; use crate::helpers::qemu::qemu_img_info; -use crate::task_common::error::TaskError; +use crate::task_common::error::{HelperCommandSnafu, TaskError}; use snafu::ResultExt; use std::path::{Path, PathBuf}; use tokio::fs; - #[derive(Clone, Debug)] pub struct ImportSource { pub file_path: PathBuf, @@ -16,16 +16,19 @@ pub struct ScanResult { pub warnings: Vec, } -pub async fn scan_import_sources(source: &Path) -> Result { +pub async fn scan_import_sources( + qemu_img: &QemuImgCmdProvider, + source: &Path, +) -> Result { let source_metadata = fs::metadata(source) .await .whatever_context::<_, TaskError>("Query filesystem metadata")?; if source_metadata.is_dir() { - scan_directory(source).await + scan_directory(qemu_img, source).await } else if source_metadata.is_file() { Ok(ScanResult { - sources: vec![scan_file(source).await?], + sources: vec![scan_file(qemu_img, source).await?], warnings: vec![], }) } else if source_metadata.is_symlink() { @@ -44,7 +47,10 @@ pub async fn scan_import_sources(source: &Path) -> Result const SUPPORTED_EXTENSIONS: &[&str] = &["qcow2", "vmdk", "img", "vhd", "vhdx"]; -async fn scan_directory(dir: &Path) -> Result { +async fn scan_directory( + qemu_img: &QemuImgCmdProvider, + dir: &Path, +) -> Result { let mut entries = fs::read_dir(dir) .await .whatever_context::<_, TaskError>("Read source directory")?; @@ -64,7 +70,7 @@ async fn scan_directory(dir: &Path) -> Result { if !is_supported { continue; } - match scan_file(&path).await { + match scan_file(qemu_img, &path).await { Ok(source) => sources.push(source), Err(e) => { warnings.push(format!( @@ -79,8 +85,13 @@ async fn scan_directory(dir: &Path) -> Result { Ok(ScanResult { sources, warnings }) } -async fn scan_file(file_path: &Path) -> Result { - let info = qemu_img_info(file_path).await?; +async fn scan_file( + qemu_img: &QemuImgCmdProvider, + file_path: &Path, +) -> Result { + let info = qemu_img_info(qemu_img.clone(), file_path) + .await + .context(HelperCommandSnafu)?; let name_part = file_path .file_name() .map(|s| s.to_string_lossy().to_string()) diff --git a/src/tasks_internal/mod.rs b/src/tasks_internal/mod.rs index 44dcc92..7d25c31 100644 --- a/src/tasks_internal/mod.rs +++ b/src/tasks_internal/mod.rs @@ -1 +1,2 @@ pub mod proxy; +pub mod qemu_img; diff --git a/src/tasks_internal/qemu_img.rs b/src/tasks_internal/qemu_img.rs new file mode 100644 index 0000000..d56563b --- /dev/null +++ b/src/tasks_internal/qemu_img.rs @@ -0,0 +1,93 @@ +use crate::helpers::helper_cmd_error::HelperCommandError; +use crate::helpers::qemu::qemu_img_cmd_provider::QemuImgCmdProvider; +use crate::helpers::qemu::qemu_img_cmd_provider::dl_win::download_qemu_img; +use crate::task_common::error::{HelperCommandSnafu, TaskError}; +use cliclack::{confirm, log}; +use snafu::ResultExt; +use std::collections::HashSet; + +pub async fn ensure_qemu_img() -> Result { + match QemuImgCmdProvider::find_bin().await { + Ok(bin) => Ok(bin), + Err(HelperCommandError::QemuImgNotFound) => { + if cfg!(target_os = "windows") { + offer_dl_qemu_img_windows().await + } else { + print_qemu_img_error().await?; + Err(HelperCommandError::QemuImgNotFound) + } + } + Err(e) => Err(e), + } +} + +pub async fn dl_qemu_img() -> Result<(), TaskError> { + offer_dl_qemu_img_windows() + .await + .context(HelperCommandSnafu)?; + + Ok(()) +} + +pub async fn offer_dl_qemu_img_windows() -> Result { + if confirm("QEMU Image tools are required to import/export. Download now?") + .initial_value(true) + .interact()? + { + download_qemu_img().await?; + QemuImgCmdProvider::find_bin().await + } else { + Err(HelperCommandError::QemuImgNotFound) + } +} + +async fn print_qemu_img_error() -> Result<(), HelperCommandError> { + log::error("qemu-img is required but was not found on your PATH.")?; + if let Some(install_hint) = get_install_hint().await { + log::error(format!("Install it with:\n\t{install_hint}"))?; + } + + Ok(()) +} + +async fn get_install_hint() -> Option<&'static str> { + if cfg!(target_os = "macos") { + //TODO: is there a package that doesn't install all of qemu? + // (we should consider shipping our own qemu-img binary on macOS) + Some("brew install qemu") + } else if cfg!(target_os = "linux") { + let os_release = tokio::fs::read_to_string("/etc/os-release").await.ok()?; + let id = parse_kv(&os_release, "ID").unwrap_or("".into()); + let id_likes: HashSet = parse_kv(&os_release, "ID_LIKE") + .map(|s| s.split(" ").map(|s| s.to_string()).collect()) + .unwrap_or_default(); + if &id == "debian" || &id == "ubuntu" || id_likes.contains("debian") { + Some("apt install qemu-utils") + } else if &id == "fedora" + || id_likes.contains("fedora") + || id_likes.contains("rhel") + || id_likes.contains("centos") + { + Some("dnf install qemu-img") + } else if &id == "arch" || id_likes.contains("arch") { + Some("pacman -S qemu-img") + } else if &id == "alpine" { + Some("apk add qemu-img") + } else { + None + } + } else { + None + } +} + +fn parse_kv(contents: &str, key: &str) -> Option { + contents.lines().find_map(|line| { + let (k, v) = line.split_once('=')?; + if k == key { + Some(v.trim_matches('"').to_lowercase()) + } else { + None + } + }) +}