diff --git a/.envrc b/.envrc deleted file mode 100644 index 1d953f4..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use nix diff --git a/Cargo.lock b/Cargo.lock index 7c81157..f9c4223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,27 +18,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] -name = "aho-corasick" -version = "1.1.3" +name = "ahash" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "memchr", + "cfg-if", + "once_cell", + "version_check", + "zerocopy", ] [[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "libc", + "memchr", ] [[package]] @@ -90,12 +87,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "anyhow" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" - [[package]] name = "arrayref" version = "0.3.9" @@ -108,6 +99,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -192,6 +194,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", + "memmap2", ] [[package]] @@ -202,9 +205,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -226,9 +229,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "jobserver", "libc", @@ -249,24 +252,18 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", "num-traits", - "serde", - "wasm-bindgen", - "windows-targets", ] [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -274,9 +271,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -298,9 +295,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" @@ -327,12 +324,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "crc32fast" version = "1.4.2" @@ -342,6 +333,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "darling" version = "0.20.10" @@ -402,6 +418,15 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -432,20 +457,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "errno" -version = "0.3.10" +name = "fallible-iterator" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fdeflate" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -589,6 +616,17 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "goblin" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ab3f32d1d77146981dea5d6b1e8fe31eedcb7013e5e00d6ccd1259a4b4d923" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "h2" version = "0.4.7" @@ -610,36 +648,39 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] [[package]] -name = "heck" -version = "0.5.0" +name = "hashbrown" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "hashlink" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] -name = "home" -version = "0.5.9" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -732,29 +773,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "1.5.0" @@ -873,12 +891,6 @@ dependencies = [ "syn", ] -[[package]] -name = "icy_sixel" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86858ae800284d596cfdefcb0ad435c3493c12f35367431bbe9b2b3858c1155b" - [[package]] name = "ident_case" version = "1.0.1" @@ -920,12 +932,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -970,10 +982,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -985,27 +998,21 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.166" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] -name = "libredox" -version = "0.1.3" +name = "libsqlite3-sys" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "bitflags 2.6.0", - "libc", - "redox_syscall", + "cc", + "pkg-config", + "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - [[package]] name = "litemap" version = "0.7.4" @@ -1044,6 +1051,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -1062,23 +1078,25 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "wasi", "windows-sys 0.52.0", ] [[package]] -name = "nanoid" -version = "0.4.0" +name = "nix" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "rand", + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", ] [[package]] @@ -1090,6 +1108,31 @@ dependencies = [ "memchr", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -1105,12 +1148,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" -[[package]] -name = "numtoa" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" - [[package]] name = "object" version = "0.36.5" @@ -1127,10 +1164,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] -name = "paste" -version = "1.0.15" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "percent-encoding" @@ -1156,11 +1193,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -1175,6 +1218,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1212,10 +1261,10 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "rustls", "socket2", - "thiserror 2.0.3", + "thiserror 2.0.6", "tokio", "tracing", ] @@ -1230,11 +1279,11 @@ dependencies = [ "getrandom", "rand", "ring", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.3", + "thiserror 2.0.6", "tinyvec", "tracing", "web-time", @@ -1300,19 +1349,65 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.5.7" +name = "rayon" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ - "bitflags 2.6.0", + "either", + "rayon-core", ] [[package]] -name = "redox_termios" -version = "0.1.3" +name = "rayon-core" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "refinery" +version = "0.8.14" +source = "git+https://github.com/rust-db/refinery#8e120f6cb866940c9ebe843cdc6a837bcf56bad8" +dependencies = [ + "refinery-core", + "refinery-macros", +] + +[[package]] +name = "refinery-core" +version = "0.8.14" +source = "git+https://github.com/rust-db/refinery#8e120f6cb866940c9ebe843cdc6a837bcf56bad8" +dependencies = [ + "async-trait", + "cfg-if", + "log", + "regex", + "rusqlite", + "serde", + "siphasher", + "thiserror 1.0.69", + "time", + "toml", + "url", + "walkdir", +] + +[[package]] +name = "refinery-macros" +version = "0.8.14" +source = "git+https://github.com/rust-db/refinery#8e120f6cb866940c9ebe843cdc6a837bcf56bad8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "refinery-core", + "regex", + "syn", +] [[package]] name = "regex" @@ -1420,25 +1515,17 @@ dependencies = [ ] [[package]] -name = "rmp" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" -dependencies = [ - "byteorder", - "num-traits", - "paste", -] - -[[package]] -name = "rmp-serde" -version = "1.3.0" +name = "rusqlite" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "byteorder", - "rmp", - "serde", + "bitflags 2.6.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", ] [[package]] @@ -1455,22 +1542,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" - -[[package]] -name = "rustix" -version = "0.38.41" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" -dependencies = [ - "bitflags 2.6.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustls" @@ -1527,20 +1601,49 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -1559,6 +1662,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1592,6 +1704,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -1609,32 +1727,70 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "soar-cli" -version = "0.4.8" +version = "0.1.0" dependencies = [ - "anyhow", - "backhand", - "base64", - "blake3", - "chrono", "clap", "futures", - "icy_sixel", - "image", "indicatif", - "libc", - "nanoid", + "nu-ansi-term 0.50.1", + "rand", + "rayon", "regex", "reqwest", - "rmp-serde", + "rusqlite", "serde", "serde_json", - "strip-ansi-escapes", - "termion", + "soar-core", + "soar-db", + "soar-dl", "tokio", + "toml", "tracing", "tracing-subscriber", - "which", - "xattr", +] + +[[package]] +name = "soar-core" +version = "0.1.0" +dependencies = [ + "blake3", + "chrono", + "futures", + "image", + "nix", + "once_cell", + "rayon", + "regex", + "reqwest", + "rusqlite", + "serde", + "soar-db", + "soar-dl", + "squishy", + "thiserror 2.0.6", + "toml", +] + +[[package]] +name = "soar-db" +version = "0.1.0" +dependencies = [ + "refinery", + "rusqlite", +] + +[[package]] +name = "soar-dl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ebc967d9e38cd1d6fc137b3dc0d79ea75dc20dfdaf114ad31cd65e7ec5f7f9" +dependencies = [ + "futures", + "regex", + "reqwest", + "serde", + "tokio", + "url", ] [[package]] @@ -1654,19 +1810,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "squishy" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "315fb6ca7507585bc9cff0d16958ffa7b3bf3fadbc32ce4b67695ccbd94e281b" +dependencies = [ + "backhand", + "goblin", + "rayon", + "thiserror 2.0.6", +] [[package]] -name = "strip-ansi-escapes" -version = "0.2.0" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" -dependencies = [ - "vte", -] +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" @@ -1682,9 +1841,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.89" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1717,18 +1876,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "termion" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eaa98560e51a2cf4f0bb884d8b2098a9ea11ecf3b7078e9c68242c74cc923a7" -dependencies = [ - "libc", - "libredox", - "numtoa", - "redox_termios", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1740,11 +1887,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.6", ] [[package]] @@ -1760,9 +1907,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", "quote", @@ -1779,6 +1926,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1806,9 +1984,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -1833,20 +2011,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -1855,11 +2032,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -1868,6 +2060,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -1907,21 +2101,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", + "nu-ansi-term 0.46.0", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "thread_local", "tracing", "tracing-core", + "tracing-serde", ] [[package]] @@ -1984,23 +2193,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "vte" -version = "0.11.1" +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" -dependencies = [ - "utf8parse", - "vte_generate_state_changes", -] +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] -name = "vte_generate_state_changes" -version = "0.1.2" +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "proc-macro2", - "quote", + "same-file", + "winapi-util", ] [[package]] @@ -2020,9 +2237,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -2031,13 +2248,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -2046,21 +2262,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2068,9 +2285,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -2081,9 +2298,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-streams" @@ -2100,9 +2317,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -2128,26 +2345,36 @@ dependencies = [ ] [[package]] -name = "which" -version = "7.0.0" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "either", - "home", - "rustix", - "winsafe", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-targets", + "windows-sys 0.59.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-registry" version = "0.2.0" @@ -2269,12 +2496,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "write16" version = "1.0.0" @@ -2296,17 +2517,6 @@ dependencies = [ "tap", ] -[[package]] -name = "xattr" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" -dependencies = [ - "libc", - "linux-raw-sys", - "rustix", -] - [[package]] name = "xz2" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index edfd562..c80197b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,10 @@ -[package] -name = "soar-cli" -version = "0.4.8" -authors = ["Rabindra Dhakal "] -description = "A modern package manager for Linux" -license = "MIT" -edition = "2021" -repository = "https://github.com/pkgforge/soar" -default-run = "soar" -exclude = ["docs"] -keywords = ["package-manager", "portable", "binary", "appimage", "linux"] - -[[bin]] -name = "soar" -path = "src/main.rs" +[workspace] +members = [ + "soar-db", + "soar-cli", + "soar-core" +] +resolver = "2" [profile.release] strip = true @@ -20,29 +12,3 @@ opt-level = "z" lto = true codegen-units = 1 panic = "abort" - -[dependencies] -anyhow = "1.0.93" -backhand = "0.18.0" -base64 = "0.22.1" -blake3 = "1.5.5" -chrono = { version = "0.4.38", features = ["serde"] } -clap = { version = "4.5.21", features = ["cargo", "derive"] } -futures = "0.3.31" -icy_sixel = "0.1.2" -image = { version = "0.25.5", default-features = false, features = ["png"] } -indicatif = "0.17.9" -libc = "0.2.166" -nanoid = "0.4.0" -regex = { version = "1.11.1", default-features = false, features = ["std", "unicode-case", "unicode-perl"] } -reqwest = { version = "0.12.9", features = ["blocking", "http2", "json", "rustls-tls", "stream"], default-features = false } -rmp-serde = "1.3.0" -serde = { version = "1.0.215", features = ["derive"] } -serde_json = "1.0.133" -strip-ansi-escapes = "0.2.0" -termion = "4.0.3" -tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } -tracing = { version = "0.1.41", default-features = false } -tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"], default-features = false } -which = "7.0.0" -xattr = { version = "1.3.1", default-features = false } diff --git a/README.md b/README.md index 1edc7b8..c813ef9 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,9 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -v, --verbose... - -q, --quiet - -j, --json + -v, --verbose... + -q, --quiet + -j, --json -h, --help Print help -V, --version Print version ``` diff --git a/default.nix b/default.nix deleted file mode 100644 index 6d599ab..0000000 --- a/default.nix +++ /dev/null @@ -1,10 +0,0 @@ -with import {}; -mkShell { - nativeBuildInputs = [ - rustc - cargo - clippy - rustfmt - rust-analyzer - ]; -} diff --git a/icons/hicolor/48x48/apps/soar.png b/icons/hicolor/48x48/apps/soar.png deleted file mode 100644 index 748f718..0000000 Binary files a/icons/hicolor/48x48/apps/soar.png and /dev/null differ diff --git a/icons/hicolor/scalable/apps/soar.svg b/icons/hicolor/scalable/apps/soar.svg deleted file mode 100644 index 42065c9..0000000 --- a/icons/hicolor/scalable/apps/soar.svg +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/soar-cli/Cargo.lock b/soar-cli/Cargo.lock new file mode 100644 index 0000000..7f5bcef --- /dev/null +++ b/soar-cli/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "soar-cli" +version = "0.1.0" diff --git a/soar-cli/Cargo.toml b/soar-cli/Cargo.toml new file mode 100644 index 0000000..9fa9992 --- /dev/null +++ b/soar-cli/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "soar-cli" +version = "0.1.0" +authors = ["Rabindra Dhakal "] +description = "A modern package manager for Linux" +license = "MIT" +edition = "2021" +repository = "https://github.com/pkgforge/soar" +default-run = "soar" +exclude = ["docs"] +keywords = ["package-manager", "portable", "binary", "appimage", "linux"] + +[[bin]] +name = "soar" +path = "src/main.rs" + +[dependencies] +clap = { version = "4.5.23", features = ["cargo", "derive"] } +futures = "0.3.31" +indicatif = "0.17.9" +nu-ansi-term = "0.50.1" +rand = "0.8.5" +rayon = "1.10.0" +regex = { version = "1.11.1", default-features = false, features = ["unicode-case", "unicode-perl", "std"] } +reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "blocking", "http2", "json", "stream"] } +rusqlite = { version = "0.32.1", features = ["bundled"] } +serde = "1.0.217" +serde_json = "1.0.133" +soar-core = { version = "0.1.0", path = "../soar-core" } +soar-db = { version = "0.1.0", path = "../soar-db" } +soar-dl = "0.1.2" +tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] } +toml = "0.8.19" +tracing = { version = "0.1.41", default-features = false } +tracing-subscriber = { version = "0.3.19", default-features = false, features = ["env-filter", "fmt", "json", "nu-ansi-term"] } diff --git a/src/cli.rs b/soar-cli/src/cli.rs similarity index 93% rename from src/cli.rs rename to soar-cli/src/cli.rs index 78c4938..9ea5e40 100644 --- a/src/cli.rs +++ b/soar-cli/src/cli.rs @@ -122,14 +122,18 @@ pub enum Commands { /// Packages to get info about #[arg(required = false)] packages: Option>, + + /// Repository to get installed packages for + #[arg(required = false)] + repo_name: Option, }, /// List all available packages #[clap(name = "list", visible_alias = "ls")] ListPackages { - /// Which collection to get the packages from + /// Which repository to get the packages from #[arg(required = false)] - collection: Option, + repo_name: Option, }, /// Inspect package build log @@ -177,7 +181,7 @@ pub enum Commands { #[clap(name = "download", visible_alias = "dl")] Download { /// Links to files - #[arg(required = true)] + #[arg(required = false)] links: Vec, /// Skip all prompts and use first @@ -199,6 +203,14 @@ pub enum Commands { /// Check if the asset contains given string #[arg(required = false, short, long = "exclude")] exclude_keywords: Option>, + + /// Github project + #[arg(required = false, long)] + github: Vec, + + /// Gitlab project + #[arg(required = false, long)] + gitlab: Vec, }, /// Health check diff --git a/soar-cli/src/download.rs b/soar-cli/src/download.rs new file mode 100644 index 0000000..7c0d386 --- /dev/null +++ b/soar-cli/src/download.rs @@ -0,0 +1,194 @@ +use std::sync::Arc; + +use indicatif::HumanBytes; +use regex::Regex; +use serde::Deserialize; +use soar_core::SoarResult; +use soar_dl::{ + common::{PlatformDownloadOptions, Release, ReleaseAsset, ReleaseHandler, ReleasePlatform}, + downloader::{DownloadOptions, DownloadState, Downloader}, + github::{Github, GithubRelease}, + gitlab::{Gitlab, GitlabRelease}, +}; +use tracing::{error, info}; + +use crate::{ + progress::{self, create_progress_bar}, + utils::interactive_ask, +}; + +pub struct DownloadContext { + regex_patterns: Option>, + match_keywords: Option>, + exclude_keywords: Option>, + output: Option, + yes: bool, + progress_callback: Arc, +} + +pub async fn download( + links: Vec, + github: Vec, + gitlab: Vec, + regex_patterns: Option>, + match_keywords: Option>, + exclude_keywords: Option>, + output: Option, + yes: bool, +) -> SoarResult<()> { + let progress_bar = create_progress_bar(); + let progress_callback = Arc::new(move |state| progress::handle_progress(state, &progress_bar)); + + let ctx = DownloadContext { + regex_patterns: regex_patterns.clone(), + match_keywords: match_keywords.clone(), + exclude_keywords: exclude_keywords.clone(), + output: output.clone(), + yes, + progress_callback: progress_callback.clone(), + }; + + handle_direct_downloads(links, output.clone(), progress_callback.clone()).await?; + + if !github.is_empty() { + handle_github_downloads(&ctx, github).await?; + } + + if !gitlab.is_empty() { + handle_gitlab_downloads(&ctx, gitlab).await?; + } + + Ok(()) +} + +pub async fn handle_direct_downloads( + links: Vec, + output: Option, + progress_callback: Arc, +) -> SoarResult<()> { + let downloader = Downloader::default(); + + for link in &links { + let options = DownloadOptions { + url: link.clone(), + output_path: output.clone(), + progress_callback: Some(progress_callback.clone()), + }; + + info!("Downloading using direct link: {}", link); + let _ = downloader + .download(options) + .await + .map_err(|e| error!("{}", e)); + } + + Ok(()) +} + +fn create_platform_options(ctx: &DownloadContext, tag: Option) -> PlatformDownloadOptions { + let asset_regexes = ctx + .regex_patterns + .clone() + .map(|patterns| { + patterns + .iter() + .map(|pattern| Regex::new(pattern)) + .collect::, regex::Error>>() + }) + .transpose() + .unwrap() + .unwrap_or_default(); + + PlatformDownloadOptions { + output_path: ctx.output.clone(), + progress_callback: Some(ctx.progress_callback.clone()), + tag, + regex_patterns: asset_regexes, + match_keywords: ctx.match_keywords.clone().unwrap_or_default(), + exclude_keywords: ctx.exclude_keywords.clone().unwrap_or_default(), + exact_case: false, + } +} + +async fn handle_platform_download( + ctx: &DownloadContext, + handler: &ReleaseHandler

, + project: &str, +) -> SoarResult<()> +where + R: Release + for<'de> Deserialize<'de>, + A: ReleaseAsset + Clone, +{ + let (project, tag) = match project.trim().split_once('@') { + Some((proj, tag)) if !tag.trim().is_empty() => (proj, Some(tag.trim())), + _ => (project.trim_end_matches('@'), None), + }; + + let options = create_platform_options(&ctx, tag.map(String::from)); + let releases = handler.fetch_releases::(project).await?; + let assets = handler.filter_releases(&releases, &options).await?; + + let selected_asset = if assets.len() == 1 || ctx.yes { + assets[0].clone() + } else { + select_asset(&assets)? + }; + handler.download(&selected_asset, options.clone()).await?; + Ok(()) +} + +pub async fn handle_github_downloads( + ctx: &DownloadContext, + projects: Vec, +) -> SoarResult<()> { + let handler = ReleaseHandler::::new(); + for project in &projects { + info!("Fetching releases from GitHub: {}", project); + if let Err(e) = + handle_platform_download::<_, GithubRelease, _>(ctx, &handler, project).await + { + eprintln!("{}", e); + } + } + Ok(()) +} + +pub async fn handle_gitlab_downloads( + ctx: &DownloadContext, + projects: Vec, +) -> SoarResult<()> { + let handler = ReleaseHandler::::new(); + for project in &projects { + info!("Fetching releases from GitLab: {}", project); + if let Err(e) = + handle_platform_download::<_, GitlabRelease, _>(ctx, &handler, project).await + { + eprintln!("{}", e); + } + } + Ok(()) +} + +fn select_asset(assets: &[A]) -> SoarResult +where + A: Clone, + A: ReleaseAsset, +{ + info!("\nAvailable assets:"); + for (i, asset) in assets.iter().enumerate() { + let size = asset + .size() + .map(|s| format!(" ({})", HumanBytes(s))) + .unwrap_or_default(); + info!("{}. {}{}", i + 1, asset.name(), size); + } + + loop { + let max = assets.len(); + let response = interactive_ask(&format!("Select an asset (1-{max}): "))?; + match response.parse::() { + Ok(n) if n > 0 && n <= max => return Ok(assets[n - 1].clone()), + _ => error!("Invalid selection, please try again."), + } + } +} diff --git a/soar-cli/src/inspect.rs b/soar-cli/src/inspect.rs new file mode 100644 index 0000000..fe51a46 --- /dev/null +++ b/soar-cli/src/inspect.rs @@ -0,0 +1,96 @@ +use std::fmt::Display; + +use futures::StreamExt; +use indicatif::HumanBytes; +use soar_core::{ + database::{ + models::Package, + packages::{get_packages_with_filter, PackageFilter}, + }, + package::query::PackageQuery, + SoarResult, +}; +use tracing::{error, info}; + +use crate::{state::AppState, utils::interactive_ask}; + +pub enum InspectType { + BuildLog, + BuildScript, +} + +impl Display for InspectType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InspectType::BuildLog => write!(f, "log"), + InspectType::BuildScript => write!(f, "script"), + } + } +} + +pub async fn inspect_log(package: &str, inspect_type: InspectType) -> SoarResult<()> { + let state = AppState::new().await?; + let repo_db = state.repo_db().clone(); + + let query = PackageQuery::try_from(package)?; + let filter = PackageFilter::from_query(query); + + let packages: Vec = get_packages_with_filter(repo_db, 1024, filter)? + .into_iter() + .filter_map(Result::ok) + .collect(); + + if packages.is_empty() { + error!("Package {package} not found"); + } else { + let first_pkg = packages.first().unwrap(); + + let url = if matches!(inspect_type, InspectType::BuildLog) { + &first_pkg.build_log + } else if first_pkg.build_script.starts_with("https://github.com") { + &first_pkg + .build_script + .replacen("/tree/", "/raw/refs/heads/", 1) + .replacen("/blob/", "/raw/refs/heads/", 1) + } else { + &first_pkg.build_script + }; + + let resp = reqwest::get(url).await?; + if !resp.status().is_success() { + error!( + "Error fetching build {inspect_type} from {} [{}]", + url, + resp.status() + ); + return Ok(()); + } + + let content_length = resp.content_length().unwrap_or_default(); + if content_length > 1_048_576 { + let response = interactive_ask( + "The {inspect_type} file is too large. Do you really want to view it (y/N)?", + )?; + if !response.starts_with('y') { + return Ok(()); + } + } + + info!( + "Fetching build {inspect_type} from {} [{}]", + url, + HumanBytes(content_length) + ); + + let mut stream = resp.bytes_stream(); + let mut content = Vec::new(); + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + content.extend_from_slice(&chunk); + } + let output = String::from_utf8_lossy(&content).replace("\r", "\n"); + + info!("\n{}", output); + } + Ok(()) +} diff --git a/soar-cli/src/install.rs b/soar-cli/src/install.rs new file mode 100644 index 0000000..3fbf516 --- /dev/null +++ b/soar-cli/src/install.rs @@ -0,0 +1,372 @@ +use std::{ + os::unix::fs, + path::PathBuf, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, Mutex, + }, +}; + +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use rand::{distributions::Alphanumeric, Rng}; +use rusqlite::Connection; +use soar_core::{ + constants::{bin_path, packages_path}, + database::{ + models::{InstalledPackage, Package}, + packages::{get_installed_packages_with_filter, get_packages_with_filter, PackageFilter}, + }, + error::SoarError, + package::{formats::common::integrate_package, install::PackageInstaller, query::PackageQuery}, + utils::calculate_checksum, + SoarResult, +}; +use soar_dl::downloader::DownloadState; +use tokio::sync::Semaphore; +use tracing::{error, info, warn}; + +use crate::{ + progress::{self, create_progress_bar}, + state::AppState, + utils::interactive_ask, +}; + +#[derive(Clone)] +pub struct InstallTarget { + pub package: Package, + pub existing_install: Option, +} + +#[derive(Clone)] +pub struct InstallContext { + pub multi_progress: Arc, + pub total_progress_bar: ProgressBar, + pub semaphore: Arc, + pub installed_count: Arc, + pub total_packages: usize, + pub portable: Option, + pub portable_home: Option, + pub portable_config: Option, +} + +pub fn create_install_context( + total_packages: usize, + parallel_limit: usize, + portable: Option, + portable_home: Option, + portable_config: Option, +) -> InstallContext { + let multi_progress = Arc::new(MultiProgress::new()); + let total_progress_bar = multi_progress.add(ProgressBar::new(total_packages as u64)); + total_progress_bar.set_style(ProgressStyle::with_template("Installing {pos}/{len}").unwrap()); + + InstallContext { + multi_progress, + total_progress_bar, + semaphore: Arc::new(Semaphore::new(parallel_limit)), + installed_count: Arc::new(AtomicU64::new(0)), + total_packages, + portable, + portable_home, + portable_config, + } +} + +pub async fn install_packages( + packages: &[String], + force: bool, + yes: bool, + portable: Option, + portable_home: Option, + portable_config: Option, +) -> SoarResult<()> { + let state = AppState::new().await?; + let repo_db = state.repo_db().clone(); + let core_db = state.core_db().clone(); + + let install_targets = resolve_packages(repo_db, core_db.clone(), packages, yes, force)?; + + let install_context = create_install_context( + install_targets.len(), + state.config().parallel_limit.unwrap_or(1) as usize, + portable, + portable_home, + portable_config, + ); + + perform_installation(install_context, install_targets, core_db, force).await +} + +fn resolve_packages( + db: Arc>, + core_db: Arc>, + packages: &[String], + yes: bool, + force: bool, +) -> SoarResult> { + let mut install_targets = Vec::new(); + + for package in packages { + let query = PackageQuery::try_from(package.as_str())?; + let filter = PackageFilter::from_query(query); + + let existing_install = get_existing_install(&core_db, &filter)?; + + if existing_install.is_some() { + warn!( + "{} is already installed - {}", + package, + if force { "reinstalling" } else { "skipping" } + ); + if !force { + continue; + } + } + + if let Some(package) = select_package(db.clone(), package, &filter, yes, &existing_install)? + { + install_targets.push(InstallTarget { + package, + existing_install, + }); + } + } + + Ok(install_targets) +} + +fn get_existing_install( + core_db: &Arc>, + filter: &PackageFilter, +) -> SoarResult> { + let installed_pkgs: Vec = + get_installed_packages_with_filter(core_db.clone(), 128, filter.clone())? + .into_iter() + .filter_map(Result::ok) + .collect(); + + Ok(installed_pkgs.into_iter().next()) +} + +fn select_package( + db: Arc>, + package_name: &str, + filter: &PackageFilter, + yes: bool, + existing_install: &Option, +) -> SoarResult> { + let filter = if let Some(existing) = existing_install { + PackageFilter { + repo_name: Some(existing.repo_name.clone()), + collection: Some(existing.collection.clone()), + exact_pkg_name: Some(existing.pkg_name.clone()), + family: Some(existing.family.clone()), + ..Default::default() + } + } else { + filter.clone() + }; + let pkgs: Vec = get_packages_with_filter(db, 1024, filter)? + .into_iter() + .filter_map(Result::ok) + .collect(); + + match pkgs.len() { + 0 => { + error!("Package {package_name} not found"); + Ok(None) + } + 1 => Ok(pkgs.into_iter().next()), + _ if yes => Ok(pkgs.into_iter().next()), + _ => select_package_interactively(pkgs, package_name), + } +} + +fn select_package_interactively( + pkgs: Vec, + package_name: &str, +) -> SoarResult> { + info!("Multiple packages found for {package_name}"); + for (idx, pkg) in pkgs.iter().enumerate() { + info!( + "[{}] {}/{}-{}:{}", + idx + 1, + pkg.family, + pkg.pkg_name, + pkg.version, + pkg.repo_name + ); + } + + let selection = get_valid_selection(pkgs.len())?; + Ok(pkgs.into_iter().nth(selection)) +} + +fn get_valid_selection(max: usize) -> SoarResult { + loop { + let response = interactive_ask("Select a package: ")?; + match response.parse::() { + Ok(n) if n > 0 && n <= max => return Ok(n - 1), + _ => error!("Invalid selection, please try again."), + } + } +} + +pub async fn perform_installation( + ctx: InstallContext, + targets: Vec, + core_db: Arc>, + force: bool, +) -> SoarResult<()> { + let mut handles = Vec::new(); + let fixed_width = 30; + + if targets.is_empty() { + info!("No packages to install"); + return Ok(()); + } + + for (idx, target) in targets.iter().enumerate() { + let handle = spawn_installation_task( + &ctx, + target.clone(), + core_db.clone(), + idx, + fixed_width, + force, + ) + .await; + handles.push(handle); + } + + for handle in handles { + handle + .await + .map_err(|err| SoarError::Custom(format!("Join handle error: {}", err)))?; + } + + ctx.total_progress_bar.finish_and_clear(); + info!( + "Installed {}/{} packages", + ctx.installed_count.load(Ordering::Relaxed), + ctx.total_packages + ); + + Ok(()) +} + +async fn spawn_installation_task( + ctx: &InstallContext, + target: InstallTarget, + core_db: Arc>, + idx: usize, + fixed_width: usize, + force: bool, +) -> tokio::task::JoinHandle<()> { + let permit = ctx.semaphore.clone().acquire_owned().await.unwrap(); + let progress_bar = ctx + .multi_progress + .insert_from_back(1, create_progress_bar()); + + let message = format!( + "[{}/{}] {}/{}", + idx + 1, + ctx.total_packages, + target.package.family, + target.package.pkg_name + ); + let message = if message.len() > fixed_width { + format!("{:.width$}", message, width = fixed_width) + } else { + format!("{:, + core_db: Arc>, + force: bool, +) -> SoarResult<()> { + let (install_dir, real_bin, bin_name) = if let Some(existing) = target.existing_install { + let install_dir = PathBuf::from(existing.installed_path); + let real_bin = install_dir.join(&target.package.pkg); + let bin_name = existing + .bin_path + .map(PathBuf::from) + .unwrap_or_else(|| bin_path().join(&target.package.pkg)); + + if force { + if let Err(e) = std::fs::remove_dir_all(&install_dir) { + warn!("Failed to clean up existing installation: {}", e); + } + if let Err(e) = std::fs::remove_file(&bin_name) { + warn!("Failed to remove existing symlink: {}", e); + } + } + + (install_dir, real_bin, bin_name) + } else { + let rand_str: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(12) + .map(char::from) + .collect(); + + let install_dir = packages_path().join(format!("{}-{}", target.package.pkg_name, rand_str)); + let real_bin = install_dir.join(&target.package.pkg); + let bin_name = bin_path().join(&target.package.pkg); + + (install_dir, real_bin, bin_name) + }; + + let installer = PackageInstaller::new( + target.package.clone(), + install_dir, + Some(progress_callback), + core_db, + false, + ) + .await?; + + installer.install().await?; + + let final_checksum = calculate_checksum(&real_bin)?; + let symlink_bin = bin_path().join(&bin_name); + fs::symlink(&real_bin, &symlink_bin)?; + installer.record(&final_checksum, &symlink_bin).await?; + + integrate_package( + &real_bin, + &target.package, + ctx.portable.clone(), + ctx.portable_home.clone(), + ctx.portable_config.clone(), + ) + .await?; + + Ok(()) +} diff --git a/soar-cli/src/list.rs b/soar-cli/src/list.rs new file mode 100644 index 0000000..af31ed7 --- /dev/null +++ b/soar-cli/src/list.rs @@ -0,0 +1,263 @@ +use nu_ansi_term::Color::{Blue, Cyan, Green, Magenta, Red, Yellow}; +use soar_core::{ + database::{ + models::InstalledPackage, + packages::{get_installed_packages_with_filter, get_packages_with_filter, PackageFilter}, + }, + utils::parse_size, + SoarResult, +}; +use tracing::info; + +use crate::state::AppState; + +pub async fn search_packages( + query: String, + case_sensitive: bool, + limit: Option, +) -> SoarResult<()> { + let state = AppState::new().await?; + let repo_db = state.repo_db().clone(); + let core_db = state.core_db().clone(); + + let packages = get_packages_with_filter( + repo_db, + 1024, + PackageFilter { + pkg_name: Some(query), + exact_case: case_sensitive, + ..Default::default() + }, + )?; + + let mut count = 0; + let show_count = limit.or(state.config().search_limit).unwrap_or(20); + + for package in packages { + count += 1; + if count > show_count { + continue; + } + let package = package?; + let filter = PackageFilter { + repo_name: Some(package.repo_name.clone()), + exact_pkg_name: Some(package.pkg_name.clone()), + family: Some(package.family), + ..Default::default() + }; + + let installed_pkgs: Vec = + get_installed_packages_with_filter(core_db.clone(), 128, filter.clone())? + .into_iter() + .filter_map(Result::ok) + .collect(); + + let mut install_status = "-"; + if !installed_pkgs.is_empty() { + if installed_pkgs.first().unwrap().is_installed { + install_status = "+"; + } else { + install_status = "?"; + } + } + + info!( + pkg_name = %package.pkg_name, + description = %package.description, + version = %package.version, + repo_name = %package.repo_name, + "[{}] {}-{}:{} - {}", + install_status, + Blue.paint(package.pkg_name.clone()), + Magenta.paint(package.version.clone()), + Cyan.paint(package.repo_name.clone()), + package.description + ); + } + + info!( + "{}", + Red.paint(format!( + "Showing {} of {}", + std::cmp::min(show_count, count), + count + )) + ); + + Ok(()) +} + +pub async fn query_package(query: String) -> SoarResult<()> { + let state = AppState::new().await?; + let repo_db = state.repo_db().clone(); + + let packages = get_packages_with_filter( + repo_db, + 1024, + PackageFilter { + exact_pkg_name: Some(query), + ..Default::default() + }, + )?; + + for package in packages { + let package = package?; + info!( + pkg_name = %package.pkg_name, + family = %package.family, + repo_name = %package.repo_name, + description = %package.description, + homepage = %package.homepage, + source_url = %package.source_url, + version = %package.version, + checksum = %package.checksum, + size = %package.size, + download_url = %package.download_url, + build_date = %package.build_date, + build_log = %package.build_log, + build_script = %package.build_script, + category = %package.category, + concat!( + "\n{}: {} ({}/{1}:{})\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}\n", + "{}: {}" + ), + Red.paint("Name"), Green.paint(package.pkg_name.clone()), Cyan.paint(package.family.clone()), Red.paint(package.repo_name.clone()), + Red.paint("Description"), Yellow.paint(package.description.clone()), + Red.paint("Homepage"), Blue.paint(package.homepage.clone()), + Red.paint("Source"), Blue.paint(package.source_url.clone()), + Red.paint("Version"), Magenta.paint(package.version.clone()), + Red.paint("Checksum"), Magenta.paint(package.checksum.clone()), + Red.paint("Size"), Magenta.paint(package.size.clone()), + Red.paint("Download URL"), Blue.paint(package.download_url.clone()), + Red.paint("Build Date"), Magenta.paint(package.build_date.clone()), + Red.paint("Build Log"), Blue.paint(package.build_log.clone()), + Red.paint("Build Script"), Blue.paint(package.build_script.clone()), + Red.paint("Category"), Cyan.paint(package.category.clone()) + ); + } + + Ok(()) +} + +pub async fn list_packages(repo_name: Option) -> SoarResult<()> { + let state = AppState::new().await?; + let repo_db = state.repo_db().clone(); + let core_db = state.core_db().clone(); + + let packages = get_packages_with_filter( + repo_db, + 1024, + PackageFilter { + repo_name: repo_name.clone(), + ..Default::default() + }, + )?; + + for package in packages { + let package = package?; + let filter = PackageFilter { + repo_name: Some(package.repo_name.clone()), + exact_pkg_name: Some(package.pkg_name.clone()), + family: Some(package.family), + ..Default::default() + }; + + let installed_pkgs: Vec = + get_installed_packages_with_filter(core_db.clone(), 128, filter.clone())? + .into_iter() + .filter_map(Result::ok) + .collect(); + + let mut install_status = "-"; + if !installed_pkgs.is_empty() { + if installed_pkgs.first().unwrap().is_installed { + install_status = "+"; + } else { + install_status = "?"; + } + } + + info!( + pkg_name = %package.pkg_name, + version = %package.version, + repo_name = %package.repo_name, + "[{}] {}-{}:{}", + install_status, + Red.paint(package.pkg_name.clone()), + package.version, + package.repo_name + ); + } + + Ok(()) +} + +pub async fn list_installed_packages(repo_name: Option) -> SoarResult<()> { + let state = AppState::new().await?; + let core_db = state.core_db().clone(); + + let filter = PackageFilter { + repo_name, + ..Default::default() + }; + let packages = get_installed_packages_with_filter(core_db.clone(), 128, filter.clone())?; + + let mut count = 0; + let mut broken_count = 0; + let mut total_size = 0; + let mut broken_size = 0; + + for package in packages { + let package = package?; + + if package.is_installed { + info!( + pkg_name = %package.pkg_name, + version = %package.version, + repo_name = %package.repo_name, + installed_date = %package.installed_date.clone().unwrap(), + size = %package.size, + "{}-{}:{} ({}) ({})", + Red.paint(package.pkg_name.clone()), + package.version, + package.repo_name, + package.installed_date.clone().unwrap(), + package.size + ); + + count += 1; + total_size += parse_size(&package.size).unwrap_or(0); + } else { + broken_count += 1; + broken_size += parse_size(&package.size).unwrap_or(0); + } + } + + info!( + total_count = %count, + broken_count = %broken_count, + total_size = %total_size, + "Total: {} ({})", + count, + total_size, + ); + info!( + broken_count = %broken_count, + total_size = %broken_size, + "Broken: {} ({})", + broken_count, + broken_size + ); + + Ok(()) +} diff --git a/src/core/log.rs b/soar-cli/src/logging.rs similarity index 59% rename from src/core/log.rs rename to soar-cli/src/logging.rs index 98cf207..fa80f83 100644 --- a/src/core/log.rs +++ b/soar-cli/src/logging.rs @@ -1,3 +1,4 @@ +use nu_ansi_term::Color::{Blue, Magenta, Red, Yellow}; use tracing::{Event, Level, Subscriber}; use tracing_subscriber::{ fmt::{ @@ -10,7 +11,18 @@ use tracing_subscriber::{ use crate::cli::Args; -use super::color::{Color, ColorExt}; +#[derive(Default)] +struct MessageVisitor { + message: Option, +} + +impl tracing::field::Visit for MessageVisitor { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + self.message = Some(format!("{:?}", value)); + } + } +} pub struct CustomFormatter; @@ -21,50 +33,26 @@ where { fn format_event( &self, - ctx: &FmtContext<'_, S, N>, + _: &FmtContext<'_, S, N>, mut writer: Writer<'_>, event: &Event<'_>, ) -> std::fmt::Result { + let mut visitor = MessageVisitor::default(); + event.record(&mut visitor); + match *event.metadata().level() { - Level::TRACE => write!(writer, "{} ", "[TRACE]".color(Color::BrightMagenta)), - Level::DEBUG => write!(writer, "{} ", "[DEBUG]".color(Color::BrightBlue)), + Level::TRACE => write!(writer, "{} ", Magenta.paint("[TRACE]")), + Level::DEBUG => write!(writer, "{} ", Blue.paint("[DEBUG]")), Level::INFO => write!(writer, ""), - Level::WARN => write!(writer, "{} ", "[WARN]".color(Color::BrightYellow)), - Level::ERROR => write!(writer, "{} ", "[ERROR]".color(Color::BrightRed)), + Level::WARN => write!(writer, "{} ", Yellow.paint("[WARN]")), + Level::ERROR => write!(writer, "{} ", Red.paint("[ERROR]")), }?; - ctx.field_format().format_fields(writer.by_ref(), event)?; - - writeln!(writer) - } -} - -pub struct CleanJsonFormatter; - -impl FormatEvent for CleanJsonFormatter -where - S: Subscriber + for<'a> LookupSpan<'a>, - N: for<'a> FormatFields<'a> + 'static, -{ - fn format_event( - &self, - ctx: &FmtContext<'_, S, N>, - mut writer: Writer<'_>, - event: &Event<'_>, - ) -> std::fmt::Result { - let mut fields_output = String::new(); - - let temp_writer = Writer::new(&mut fields_output); - ctx.field_format().format_fields(temp_writer, event)?; - let clean_fields = strip_ansi_escapes::strip_str(&fields_output); - - let json = serde_json::json!({ - "level": event.metadata().level().to_string(), - "fields": clean_fields, - "target": event.metadata().target(), - }); - - writeln!(writer, "{}", json) + if let Some(message) = visitor.message { + writeln!(writer, "{}", message) + } else { + writeln!(writer, "") + } } } @@ -110,7 +98,7 @@ pub fn setup_logging(args: &Args) { }; let builder = fmt::Subscriber::builder() - .with_env_filter(format!("soar_cli={}", filter_level)) + .with_env_filter(format!("soar={}", filter_level)) .with_target(false) .with_thread_ids(false) .with_thread_names(false) @@ -122,7 +110,7 @@ pub fn setup_logging(args: &Args) { .without_time(); let subscriber: Box = if args.json { - Box::new(builder.event_format(CleanJsonFormatter).finish()) + Box::new(builder.json().flatten_event(true).finish()) } else { Box::new(builder.event_format(CustomFormatter).finish()) }; diff --git a/soar-cli/src/main.rs b/soar-cli/src/main.rs new file mode 100644 index 0000000..2d2a96a --- /dev/null +++ b/soar-cli/src/main.rs @@ -0,0 +1,175 @@ +use std::{env, io::Read, sync::Arc}; + +use clap::Parser; +use cli::Args; +use download::download; +use inspect::{inspect_log, InspectType}; +use install::install_packages; +use list::{list_installed_packages, list_packages, query_package, search_packages}; +use logging::setup_logging; +use progress::create_progress_bar; +use remove::remove_packages; +use run::run_package; +use self_actions::process_self_action; +use soar_core::{ + config::generate_default_config, + constants::{bin_path, cache_path, db_path, packages_path, repositories_path, root_path}, + database::packages::get_installed_packages, + utils::setup_required_paths, + SoarResult, +}; +use soar_dl::downloader::{DownloadOptions, Downloader}; +use tracing::{error, info}; +use update::update_packages; + +mod cli; +mod download; +mod inspect; +mod install; +mod list; +mod logging; +mod progress; +mod remove; +mod run; +mod self_actions; +mod state; +mod update; +mod utils; + +async fn handle_cli() -> SoarResult<()> { + let mut args = env::args().collect::>(); + let self_bin = args.first().unwrap().clone(); + let self_version = env!("CARGO_PKG_VERSION"); + + let mut i = 0; + while i < args.len() { + if args[i] == "-" { + let mut stdin = std::io::stdin(); + let mut buffer = String::new(); + if stdin.read_to_string(&mut buffer).is_ok() { + let stdin_args = buffer.split_whitespace().collect::>(); + args.remove(i); + args.splice(i..i, stdin_args.into_iter().map(String::from)); + } else { + i += 1; + } + } else { + i += 1; + } + } + + let args = Args::parse_from(args); + + setup_logging(&args); + + match args.command { + cli::Commands::Install { + packages, + force, + yes, + portable, + portable_home, + portable_config, + } => { + if portable.is_some() && (portable_home.is_some() || portable_config.is_some()) { + error!("--portable cannot be used with --portable-home or --portable-config"); + std::process::exit(1); + } + + let portable = portable.map(|p| p.unwrap_or_default()); + let portable_home = portable_home.map(|p| p.unwrap_or_default()); + let portable_config = portable_config.map(|p| p.unwrap_or_default()); + + install_packages( + &packages, + force, + yes, + portable, + portable_home, + portable_config, + ) + .await?; + } + cli::Commands::Search { + query, + case_sensitive, + limit, + } => { + search_packages(query, case_sensitive, limit).await?; + } + cli::Commands::Query { query } => { + query_package(query).await?; + } + cli::Commands::Remove { packages, exact } => { + remove_packages(&packages, exact).await?; + } + cli::Commands::Sync => unreachable!(), + cli::Commands::Update { packages } => { + update_packages(packages).await?; + } + cli::Commands::ListInstalledPackages { + packages, + repo_name, + } => { + list_installed_packages(repo_name).await?; + } + cli::Commands::ListPackages { repo_name } => { + list_packages(repo_name).await?; + } + cli::Commands::Log { package } => inspect_log(&package, InspectType::BuildLog).await?, + cli::Commands::Inspect { package } => { + inspect_log(&package, InspectType::BuildScript).await? + } + cli::Commands::Run { yes, command } => { + run_package(command.as_ref()).await?; + } + cli::Commands::Use { package } => unreachable!(), + cli::Commands::Download { + links, + yes, + output, + regex_patterns, + match_keywords, + exclude_keywords, + github, + gitlab, + } => { + download( + links, + github, + gitlab, + regex_patterns, + match_keywords, + exclude_keywords, + output, + yes, + ) + .await?; + } + cli::Commands::Health => unreachable!(), + cli::Commands::DefConfig => generate_default_config()?, + cli::Commands::Env => { + info!("SOAR_ROOT={}", root_path().display()); + info!("SOAR_BIN={}", bin_path().display()); + info!("SOAR_DB={}", db_path().display()); + info!("SOAR_CACHE={}", cache_path().display()); + info!("SOAR_PACKAGE={}", packages_path().display()); + info!("SOAR_REPOSITORIES={}", repositories_path().display()); + } + cli::Commands::Build { files } => unreachable!(), + cli::Commands::SelfCmd { action } => { + process_self_action(&action, self_bin, self_version).await?; + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() { + setup_required_paths().unwrap(); + + if let Err(err) = handle_cli().await { + error!("{}", err); + }; +} diff --git a/soar-cli/src/progress.rs b/soar-cli/src/progress.rs new file mode 100644 index 0000000..af446fe --- /dev/null +++ b/soar-cli/src/progress.rs @@ -0,0 +1,50 @@ +use indicatif::{HumanBytes, ProgressBar, ProgressState, ProgressStyle}; +use soar_dl::downloader::DownloadState; + +pub fn create_progress_bar() -> ProgressBar { + let progress_bar = ProgressBar::new(0); + let style = ProgressStyle::with_template( + "{msg}[{wide_bar:.green/white}] {speed:14} {computed_bytes:22}", + ) + .unwrap() + .with_key("computed_bytes", format_bytes) + .with_key("speed", format_speed) + .progress_chars("━━"); + progress_bar.set_style(style); + progress_bar +} + +fn format_bytes(state: &ProgressState, w: &mut dyn std::fmt::Write) { + write!( + w, + "{}/{}", + HumanBytes(state.pos()), + HumanBytes(state.len().unwrap_or(state.pos())) + ) + .unwrap(); +} + +fn format_speed(state: &ProgressState, w: &mut dyn std::fmt::Write) { + let speed = calculate_speed(state.pos(), state.elapsed().as_secs_f64()); + write!(w, "{}/s", HumanBytes(speed)).unwrap(); +} + +fn calculate_speed(pos: u64, elapsed: f64) -> u64 { + if elapsed > 0.0 { + (pos as f64 / elapsed) as u64 + } else { + 0 + } +} + +pub fn handle_progress(state: DownloadState, progress_bar: &ProgressBar) { + match state { + DownloadState::Progress(progress) => { + if let Some(total) = progress.total_bytes { + progress_bar.set_length(total); + } + progress_bar.set_position(progress.bytes_downloaded); + } + DownloadState::Complete => progress_bar.finish(), + } +} diff --git a/soar-cli/src/remove.rs b/soar-cli/src/remove.rs new file mode 100644 index 0000000..7f48e7a --- /dev/null +++ b/soar-cli/src/remove.rs @@ -0,0 +1,42 @@ +use soar_core::{ + database::{ + models::InstalledPackage, + packages::{get_installed_packages_with_filter, PackageFilter}, + }, + package::{query::PackageQuery, remove::PackageRemover}, + SoarResult, +}; +use tracing::{info, warn}; + +use crate::state::AppState; + +pub async fn remove_packages(packages: &[String], exact: bool) -> SoarResult<()> { + let state = AppState::new().await?; + let core_db = state.core_db().clone(); + + for package in packages { + let repo_db = state.repo_db().clone(); + + let query = PackageQuery::try_from(package.as_str())?; + let filter = PackageFilter::from_query(query); + + let installed_pkgs: Vec = + get_installed_packages_with_filter(core_db.clone(), 128, filter.clone())? + .into_iter() + .filter_map(Result::ok) + .collect(); + + let installed_pkg = installed_pkgs.first().unwrap(); + if !installed_pkg.is_installed { + warn!("Package {} is not installed.", package); + continue; + } + + let remover = PackageRemover::new(installed_pkg.clone(), repo_db).await; + remover.remove().await?; + + info!("Removed {}", installed_pkg.pkg_name); + } + + Ok(()) +} diff --git a/soar-cli/src/run.rs b/soar-cli/src/run.rs new file mode 100644 index 0000000..9a73e10 --- /dev/null +++ b/soar-cli/src/run.rs @@ -0,0 +1,76 @@ +use std::{fs, process::Command, sync::Arc}; + +use soar_core::{ + constants::cache_path, + database::{ + models::Package, + packages::{get_packages_with_filter, PackageFilter}, + }, + error::SoarError, + package::query::PackageQuery, + utils::calculate_checksum, + SoarResult, +}; +use soar_dl::downloader::{DownloadOptions, Downloader}; + +use crate::{ + progress::{self, create_progress_bar}, + state::AppState, + utils::interactive_ask, +}; + +pub async fn run_package(command: &[String]) -> SoarResult<()> { + let state = AppState::new().await?; + let repo_db = state.repo_db().clone(); + + let package_name = &command[0]; + let args = if command.len() > 1 { + &command[1..] + } else { + &[] + }; + + let query = PackageQuery::try_from(package_name.as_str())?; + let filter = PackageFilter::from_query(query); + let packages: Vec = get_packages_with_filter(repo_db, 1024, filter)? + .into_iter() + .filter_map(Result::ok) + .collect(); + + if packages.is_empty() { + return Err(SoarError::PackageNotFound(package_name.clone())); + } + + let package = packages.first().unwrap(); + let cache_bin = cache_path().join("bin"); + fs::create_dir_all(&cache_bin)?; + + let output_path = cache_bin.join(&package.pkg_name); + if !output_path.exists() { + let progress_bar = create_progress_bar(); + let progress_callback = Arc::new(move |state| { + progress::handle_progress(state, &progress_bar); + }); + + let downloader = Downloader::default(); + let options = DownloadOptions { + url: package.download_url.clone(), + output_path: Some(output_path.to_string_lossy().to_string()), + progress_callback: Some(progress_callback), + }; + + downloader.download(options).await?; + + let checksum = calculate_checksum(&output_path)?; + if checksum != package.checksum { + let response = interactive_ask("Invalid checksum. Do you want to continue (y/N)?")?; + if !response.to_lowercase().starts_with("y") { + return Err(SoarError::InvalidChecksum); + } + } + } + + Command::new(output_path).args(args).status()?; + + Ok(()) +} diff --git a/soar-cli/src/self_actions.rs b/soar-cli/src/self_actions.rs new file mode 100644 index 0000000..4c6bf31 --- /dev/null +++ b/soar-cli/src/self_actions.rs @@ -0,0 +1,73 @@ +use std::{env::consts::ARCH, fs}; + +use soar_core::SoarResult; +use soar_dl::{ + common::{Release, ReleaseAsset, ReleaseHandler}, + downloader::{DownloadOptions, Downloader}, + github::{Github, GithubRelease}, +}; +use tracing::{error, info}; + +use crate::cli::SelfAction; + +pub async fn process_self_action( + action: &SelfAction, + self_bin: String, + self_version: &str, +) -> SoarResult<()> { + match action { + SelfAction::Update => { + let is_nightly = self_version.starts_with("nightly"); + let handler = ReleaseHandler::::new(); + let releases = handler + .fetch_releases::("pkgforge/soar") + .await + .unwrap(); + + let release = releases.iter().find(|rel| { + if is_nightly { + rel.tag_name().starts_with("nightly") && rel.name() != self_version + } else { + rel.tag_name() + .trim_start_matches("v") + .parse::() + .map(|v| v > self_version.parse::().unwrap()) + .unwrap_or(false) + } + }); + + if let Some(release) = release { + let assets = release.assets(); + let asset = assets + .iter() + .find(|a| { + a.name.contains(ARCH) && !a.name.contains("tar") && !a.name.contains("sum") + }) + .unwrap(); + let downloader = Downloader::default(); + let options = DownloadOptions { + url: asset.download_url().to_string(), + output_path: Some(self_bin), + progress_callback: None, + }; + downloader.download(options).await?; + info!("Soar updated to {}", release.tag_name()); + } else { + eprintln!("No updates found."); + } + } + SelfAction::Uninstall => { + match fs::remove_file(self_bin) { + Ok(_) => { + info!("Soar has been uninstalled successfully."); + info!("You should remove soar config and data files manually."); + } + Err(err) => { + error!("{}\nFailed to uninstall soar.", err.to_string()); + } + }; + } + }; + + Ok(()) +} diff --git a/soar-cli/src/state.rs b/soar-cli/src/state.rs new file mode 100644 index 0000000..1194cee --- /dev/null +++ b/soar-cli/src/state.rs @@ -0,0 +1,83 @@ +use std::{ + fs::{self, File}, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use rusqlite::Connection; +use soar_core::{ + config::Config, constants::db_path, database::connection::Database, metadata::fetch_metadata, + SoarResult, +}; + +#[derive(Clone)] +pub struct AppState { + inner: Arc, +} + +struct AppStateInner { + config: &'static Config, + repo_db: Database, + core_db: Database, +} + +impl AppState { + pub async fn new() -> SoarResult { + let config = Config::get()?; + + Self::init_repo_dbs(config).await?; + let repo_db = Self::create_repo_db(config)?; + let core_db = Self::create_core_db()?; + + Ok(Self { + inner: Arc::new(AppStateInner { + config, + repo_db, + core_db, + }), + }) + } + + async fn init_repo_dbs(config: &Config) -> SoarResult<()> { + for repo in &config.repositories { + let db_file = repo.get_path().join("metadata.db"); + if !db_file.exists() { + fs::create_dir_all(repo.get_path())?; + File::create(&db_file)?; + } + fetch_metadata(repo.clone()).await?; + } + Ok(()) + } + + fn create_repo_db(config: &Config) -> SoarResult { + let repo_paths: Vec = config + .repositories + .iter() + .map(|r| r.get_path().join("metadata.db")) + .collect(); + + Database::new_multi(repo_paths.as_ref()) + } + + fn create_core_db() -> SoarResult { + let core_db_file = db_path().join("soar.db"); + if !core_db_file.exists() { + File::create(&core_db_file)?; + } + soar_db::core::init_db(&core_db_file).unwrap(); + Database::new(&core_db_file) + } + + pub fn config(&self) -> &Config { + &self.inner.config + } + + pub fn repo_db(&self) -> &Arc> { + &self.inner.repo_db.conn + } + + pub fn core_db(&self) -> &Arc> { + &self.inner.core_db.conn + } +} diff --git a/soar-cli/src/update.rs b/soar-cli/src/update.rs new file mode 100644 index 0000000..05fddda --- /dev/null +++ b/soar-cli/src/update.rs @@ -0,0 +1,120 @@ +use soar_core::{ + database::{ + models::{InstalledPackage, Package}, + packages::{ + get_all_packages, get_installed_packages, get_installed_packages_with_filter, + PackageFilter, + }, + }, + package::query::PackageQuery, + SoarResult, +}; + +use crate::{ + install::{create_install_context, perform_installation, InstallTarget}, + state::AppState, +}; + +pub async fn update_packages(packages: Option>) -> SoarResult<()> { + let state = AppState::new().await?; + let core_db = state.core_db(); + let repo_db = state.repo_db(); + + let mut update_targets = Vec::new(); + + if let Some(packages) = packages { + for package in packages { + let query = PackageQuery::try_from(package.as_str())?; + let filter = PackageFilter::from_query(query); + let installed_pkgs = get_installed_packages_with_filter(core_db.clone(), 1024, filter)?; + + for pkg in installed_pkgs { + if let Ok(pkg) = pkg { + let filter = PackageFilter { + pkg_name: Some(pkg.pkg_name.clone()), + repo_name: Some(pkg.repo_name.clone()), + collection: Some(pkg.collection.clone()), + family: Some(pkg.family.clone()), + ..Default::default() + }; + + if let Some(Ok(available_pkg)) = get_all_packages(repo_db.clone(), 1024)? + .into_iter() + .find(|p| matches_filter(p.as_ref().ok(), &filter)) + { + if needs_update(&pkg, &available_pkg) { + update_targets.push(InstallTarget { + package: available_pkg, + existing_install: Some(pkg), + }); + } + } + } + } + } + } else { + let installed_pkgs = get_installed_packages(core_db.clone(), 1024)?; + for pkg in installed_pkgs { + if let Ok(pkg) = pkg { + let filter = PackageFilter { + pkg_name: Some(pkg.pkg_name.clone()), + repo_name: Some(pkg.repo_name.clone()), + collection: Some(pkg.collection.clone()), + family: Some(pkg.family.clone()), + ..Default::default() + }; + + if let Some(Ok(available_pkg)) = get_all_packages(repo_db.clone(), 1024)? + .into_iter() + .find(|p| matches_filter(p.as_ref().ok(), &filter)) + { + if needs_update(&pkg, &available_pkg) { + update_targets.push(InstallTarget { + package: available_pkg, + existing_install: Some(pkg), + }); + } + } + } + } + }; + + let ctx = create_install_context( + update_targets.len(), + state.config().parallel_limit.unwrap_or(1) as usize, + None, + None, + None, + ); + + perform_installation(ctx, update_targets, core_db.clone(), true).await?; + + Ok(()) +} + +fn matches_filter(package: Option<&Package>, filter: &PackageFilter) -> bool { + if let Some(pkg) = package { + filter + .pkg_name + .as_ref() + .map_or(true, |n| n == &pkg.pkg_name) + && filter + .repo_name + .as_ref() + .map_or(true, |r| r == &pkg.repo_name) + && filter + .collection + .as_ref() + .map_or(true, |c| c == &pkg.collection) + && filter.family.as_ref().map_or(true, |f| f == &pkg.family) + } else { + false + } +} + +fn needs_update(installed: &InstalledPackage, available: &Package) -> bool { + if installed.version != available.version { + return available.version != installed.version; + } + installed.checksum != available.checksum +} diff --git a/soar-cli/src/utils.rs b/soar-cli/src/utils.rs new file mode 100644 index 0000000..0f506e1 --- /dev/null +++ b/soar-cli/src/utils.rs @@ -0,0 +1,14 @@ +use std::io::Write; + +use soar_core::SoarResult; + +pub fn interactive_ask(ques: &str) -> SoarResult { + print!("{}", ques); + + std::io::stdout().flush()?; + + let mut response = String::new(); + std::io::stdin().read_line(&mut response)?; + + Ok(response.trim().to_owned()) +} diff --git a/soar-core/Cargo.lock b/soar-core/Cargo.lock new file mode 100644 index 0000000..ce45168 --- /dev/null +++ b/soar-core/Cargo.lock @@ -0,0 +1,1686 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake3" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "memmap2", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +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", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[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-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "hyper" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + +[[package]] +name = "rustls" +version = "0.23.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "soar-core" +version = "0.1.0" +dependencies = [ + "blake3", + "nix", + "once_cell", + "rayon", + "reqwest", + "rusqlite", + "serde", + "toml", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "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.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +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 = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/soar-core/Cargo.toml b/soar-core/Cargo.toml new file mode 100644 index 0000000..85810a7 --- /dev/null +++ b/soar-core/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "soar-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +blake3 = { version = "1.5.5", features = ["mmap"] } +chrono = { version = "0.4.39", default-features = false, features = ["now"] } +futures = "0.3.31" +image = { version = "0.25.5", default-features = false, features = ["png"] } +nix = { version = "0.29.0", features = ["ioctl", "term", "user"] } +once_cell = "1.20.2" +rayon = "1.10.0" +regex = { version = "1.11.1", default-features = false, features = ["unicode-case", "unicode-perl", "std"] } +reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "blocking", "http2", "json", "stream"] } +rusqlite = { version = "0.32.1", features = ["bundled"] } +serde = { version = "1.0.215", features = ["derive"] } +soar-db = { version = "0.1.0", path = "../soar-db" } +soar-dl = "0.1.2" +squishy = { version = "0.3.0", features = ["appimage", "rayon"] } +thiserror = "2.0.6" +toml = "0.8.19" diff --git a/src/core/config.rs b/soar-core/src/config.rs similarity index 57% rename from src/core/config.rs rename to soar-core/src/config.rs index 5623e2b..d599c09 100644 --- a/src/core/config.rs +++ b/soar-core/src/config.rs @@ -1,20 +1,21 @@ use std::{ - collections::{HashMap, HashSet}, + collections::HashSet, env::{self, consts::ARCH}, fs, path::PathBuf, - sync::LazyLock, }; -use anyhow::Result; +use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; -use tracing::error; -use super::{ - constant::REGISTRY_PATH, - util::{home_config_path, home_data_path}, +use crate::{ + constants::repositories_path, + error::SoarError, + utils::{home_config_path, home_data_path}, }; +type Result = std::result::Result; + /// Application's configuration #[derive(Deserialize, Serialize)] pub struct Config { @@ -27,6 +28,15 @@ pub struct Config { /// Path to the directory where binary symlinks is stored. pub soar_bin: Option, + /// Path to the directory where installation database is stored. + pub soar_db: Option, + + /// Path to the directory where repositories database is stored. + pub soar_repositories: Option, + + /// Path to the directory where packages are stored. + pub soar_packages: Option, + /// A list of remote repositories to fetch packages from. pub repositories: Vec, @@ -41,7 +51,7 @@ pub struct Config { } /// Struct representing a repository configuration. -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub struct Repository { /// Name of the repository. pub name: String, @@ -52,36 +62,35 @@ pub struct Repository { /// Optional field specifying a custom metadata file for the repository. Default: /// `metadata.json` pub metadata: Option, - - /// Download Sources for different collections - pub sources: HashMap, } impl Repository { pub fn get_path(&self) -> PathBuf { - REGISTRY_PATH.join(&self.name) + repositories_path().join(&self.name) } } impl Config { + pub fn get() -> Result<&'static Config> { + static CONFIG: OnceCell = OnceCell::new(); + CONFIG.get_or_try_init(Config::new) + } + /// Creates a new configuration by loading it from the configuration file. /// If the configuration file is not found, it uses the default configuration. - pub fn new() -> Self { + pub fn new() -> Result { let home_config = home_config_path(); let pkg_config = PathBuf::from(home_config).join("soar"); - let config_path = pkg_config.join("config.json"); - - let mut config = match fs::read(&config_path) { - Ok(content) => serde_json::from_slice(&content).unwrap_or_else(|e| { - error!("Failed to parse config file: {}", e.to_string()); - std::process::exit(1); - }), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::default(), - Err(e) => { - error!("Error reading config file: {}", e.to_string()); - std::process::exit(1); - } - }; + let config_path = pkg_config.join("config.toml"); + + let mut config = match fs::read_to_string(&config_path) { + Ok(content) => match toml::from_str(&content) { + Ok(c) => Ok(c), + Err(_) => Err(SoarError::InvalidConfig), + }, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()), + Err(_) => Err(SoarError::InvalidConfig), + }?; config.soar_root = env::var("SOAR_ROOT").unwrap_or(config.soar_root); config.soar_bin = Some(env::var("SOAR_BIN").unwrap_or_else(|_| { @@ -94,30 +103,42 @@ impl Config { .soar_cache .unwrap_or_else(|| format!("{}/cache", config.soar_root)) })); + config.soar_db = Some(env::var("SOAR_DB").unwrap_or_else(|_| { + config + .soar_db + .unwrap_or_else(|| format!("{}/db", config.soar_root)) + })); + config.soar_packages = Some(env::var("SOAR_PACKAGE").unwrap_or_else(|_| { + config + .soar_packages + .unwrap_or_else(|| format!("{}/packages", config.soar_root)) + })); + config.soar_repositories = Some(env::var("SOAR_REPOSITORIES").unwrap_or_else(|_| { + config + .soar_repositories + .unwrap_or_else(|| format!("{}/packages", config.soar_root)) + })); + + if config.parallel.unwrap_or(true) { + config.parallel_limit = config.parallel_limit.or(Some(4)); + } let mut seen = HashSet::new(); for repo in &config.repositories { + if repo.name == "local" { + return Err(SoarError::InvalidConfig); + } if !seen.insert(&repo.name) { - error!("Found duplicate repo '{}'. Please rename the repo to have unique name. Aborting..", repo.name); - std::process::exit(1); + return Err(SoarError::InvalidConfig); } } - config + Ok(config) } } impl Default for Config { fn default() -> Self { - let sources = HashMap::from([ - ("bin".to_owned(), format!("https://bin.pkgforge.dev/{ARCH}")), - ( - "base".to_owned(), - format!("https://bin.pkgforge.dev/{ARCH}/Baseutils"), - ), - ("pkg".to_owned(), format!("https://pkg.pkgforge.dev/{ARCH}")), - ]); - let soar_root = env::var("SOAR_ROOT").unwrap_or_else(|_| format!("{}/soar", home_data_path())); @@ -125,11 +146,13 @@ impl Default for Config { soar_root: soar_root.clone(), soar_bin: Some(format!("{}/bin", soar_root)), soar_cache: Some(format!("{}/cache", soar_root)), + soar_db: Some(format!("{}/db", soar_root)), + soar_packages: Some(format!("{}/packages", soar_root)), + soar_repositories: Some(format!("{}/repos", soar_root)), repositories: vec![Repository { name: "pkgforge".to_owned(), url: format!("https://bin.pkgforge.dev/{ARCH}"), metadata: Some("METADATA.AIO.json".to_owned()), - sources, }], parallel: Some(true), parallel_limit: Some(4), @@ -138,29 +161,19 @@ impl Default for Config { } } -/// Initializes the global configuration by forcing the static `CONFIG` to load. -pub fn init() { - let _ = &*CONFIG; -} - -pub static CONFIG: LazyLock = LazyLock::new(Config::new); - pub fn generate_default_config() -> Result<()> { let home_config = home_config_path(); - let config_path = PathBuf::from(home_config).join("soar").join("config.json"); + let config_path = PathBuf::from(home_config).join("soar").join("config.toml"); if config_path.exists() { - error!("Default config already exists. Not overriding it."); - std::process::exit(1); + return Err(SoarError::ConfigAlreadyExists); } fs::create_dir_all(config_path.parent().unwrap())?; let def_config = Config::default(); - let serialized = serde_json::to_vec_pretty(&def_config)?; + let serialized = toml::to_string_pretty(&def_config)?; fs::write(&config_path, &serialized)?; - println!("Default config is saved at: {}", config_path.display()); - Ok(()) } diff --git a/soar-core/src/constants.rs b/soar-core/src/constants.rs new file mode 100644 index 0000000..e33378c --- /dev/null +++ b/soar-core/src/constants.rs @@ -0,0 +1,48 @@ +use std::{path::PathBuf, sync::OnceLock}; + +use crate::{config::Config, utils::build_path}; + +pub fn root_path() -> &'static PathBuf { + static ROOT_PATH: OnceLock = OnceLock::new(); + ROOT_PATH.get_or_init(|| build_path(&Config::get().unwrap().soar_root).unwrap()) +} + +pub fn bin_path() -> &'static PathBuf { + static BIN_PATH: OnceLock = OnceLock::new(); + BIN_PATH.get_or_init(|| build_path(&Config::get().unwrap().soar_bin.clone().unwrap()).unwrap()) +} + +pub fn cache_path() -> &'static PathBuf { + static CACHE_PATH: OnceLock = OnceLock::new(); + CACHE_PATH + .get_or_init(|| build_path(&Config::get().unwrap().soar_cache.clone().unwrap()).unwrap()) +} + +pub fn db_path() -> &'static PathBuf { + static DB_PATH: OnceLock = OnceLock::new(); + DB_PATH.get_or_init(|| build_path(&Config::get().unwrap().soar_db.clone().unwrap()).unwrap()) +} + +pub fn repositories_path() -> &'static PathBuf { + static REPOS_PATH: OnceLock = OnceLock::new(); + REPOS_PATH.get_or_init(|| { + build_path(&Config::get().unwrap().soar_repositories.clone().unwrap()).unwrap() + }) +} + +pub fn packages_path() -> &'static PathBuf { + static PACKAGES_PATH: OnceLock = OnceLock::new(); + PACKAGES_PATH + .get_or_init(|| build_path(&Config::get().unwrap().soar_packages.clone().unwrap()).unwrap()) +} + +pub const ELF_MAGIC_BYTES: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46]; +pub const APPIMAGE_MAGIC_BYTES: [u8; 4] = [0x41, 0x49, 0x02, 0x00]; +pub const FLATIMAGE_MAGIC_BYTES: [u8; 4] = [0x46, 0x49, 0x01, 0x00]; + +pub const PNG_MAGIC_BYTES: [u8; 8] = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; +pub const SVG_MAGIC_BYTES: [u8; 4] = [0x3c, 0x73, 0x76, 0x67]; +pub const XML_MAGIC_BYTES: [u8; 5] = [0x3c, 0x3f, 0x78, 0x6d, 0x6c]; + +pub const CAP_SYS_ADMIN: i32 = 21; +pub const CAP_MKNOD: i32 = 27; diff --git a/soar-core/src/database/connection.rs b/soar-core/src/database/connection.rs new file mode 100644 index 0000000..3a2c89c --- /dev/null +++ b/soar-core/src/database/connection.rs @@ -0,0 +1,58 @@ +use std::{ + path::Path, + sync::{Arc, Mutex}, +}; + +use rusqlite::Connection; + +use crate::error::SoarError; + +use super::{ + models::RemotePackageMetadata, repository::PackageRepository, statements::DbStatements, +}; + +type Result = std::result::Result; + +pub struct Database { + pub conn: Arc>, +} + +impl Database { + pub fn new>(path: P) -> Result { + let path = path.as_ref(); + let conn = Connection::open(path)?; + let conn = Arc::new(Mutex::new(conn)); + Ok(Database { conn }) + } + + pub fn new_multi>(paths: &[P]) -> Result { + let conn = Connection::open(&paths[0])?; + for (idx, path) in paths.iter().enumerate().skip(1) { + let path = path.as_ref(); + conn.execute( + &format!("ATTACH DATABASE '{}' AS shard{}", path.display(), idx), + [], + )?; + } + let conn = Arc::new(Mutex::new(conn)); + Ok(Database { conn }) + } + + pub fn from_json_metadata( + &self, + metadata: RemotePackageMetadata, + repo_name: &str, + ) -> Result<()> { + let mut guard = self.conn.lock().unwrap(); + let _: String = guard.query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))?; + + let tx = guard.transaction()?; + { + let statements = DbStatements::new(&tx)?; + let mut repo = PackageRepository::new(&tx, statements, repo_name); + repo.import_packages(&metadata)?; + } + tx.commit()?; + Ok(()) + } +} diff --git a/soar-core/src/database/mod.rs b/soar-core/src/database/mod.rs new file mode 100644 index 0000000..3d75cd6 --- /dev/null +++ b/soar-core/src/database/mod.rs @@ -0,0 +1,5 @@ +pub mod connection; +pub mod models; +pub mod packages; +pub mod repository; +pub mod statements; diff --git a/soar-core/src/database/models.rs b/soar-core/src/database/models.rs new file mode 100644 index 0000000..12a97ca --- /dev/null +++ b/soar-core/src/database/models.rs @@ -0,0 +1,86 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone)] +pub struct Package { + pub id: u64, + pub repo_name: String, + pub collection: String, + pub pkg: String, + pub pkg_id: String, + pub pkg_name: String, + pub app_id: Option, + pub family: String, + pub description: String, + pub version: String, + pub size: String, + pub checksum: String, + pub note: String, + pub download_url: String, + pub build_date: String, + pub build_script: String, + pub build_log: String, + pub homepage: String, + pub category: String, + pub source_url: String, + pub icon: Option, + pub desktop: Option, +} + +#[derive(Debug, Clone)] +pub struct InstalledPackage { + pub id: u64, + pub repo_name: String, + pub collection: String, + pub family: String, + pub pkg_name: String, + pub pkg: String, + pub pkg_id: Option, + pub app_id: Option, + pub description: String, + pub version: String, + pub size: String, + pub checksum: String, + pub build_date: String, + pub build_script: String, + pub build_log: String, + pub category: String, + pub bin_path: Option, + pub installed_path: String, + pub installed_date: Option, + pub disabled: bool, + pub pinned: bool, + pub is_installed: bool, + pub installed_with_family: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RemotePackageMetadata { + #[serde(flatten)] + pub collection: HashMap>, +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub struct RemotePackage { + pub pkg: String, + pub pkg_name: String, + pub description: String, + pub note: String, + pub version: String, + pub download_url: String, + pub size: String, + pub bsum: String, + pub build_date: String, + pub src_url: String, + pub homepage: String, + pub build_script: String, + pub build_log: String, + pub category: String, + pub provides: String, + pub icon: String, + pub desktop: Option, + pub pkg_id: Option, + pub pkg_family: Option, + pub app_id: Option, +} diff --git a/soar-core/src/database/packages/iterator.rs b/soar-core/src/database/packages/iterator.rs new file mode 100644 index 0000000..27b5c23 --- /dev/null +++ b/soar-core/src/database/packages/iterator.rs @@ -0,0 +1,408 @@ +use std::sync::{Arc, Mutex}; + +use rusqlite::{Connection, Row}; + +use crate::{ + database::models::{InstalledPackage, Package}, + error::SoarError, + SoarResult, +}; + +use super::{ + models::{IterationState, PackageFilter, PackageSort}, + query::QueryBuilder, + InstalledQueryBuilder, +}; + +#[derive(Debug)] +pub struct PackageIterator { + db: Arc>, + sort_method: PackageSort, + filter: PackageFilter, + state: IterationState, + buffer: Vec, + buffer_index: usize, + buffer_size: usize, + shard_index: usize, + shard_count: usize, + repo_name: Option, +} + +impl PackageIterator { + pub fn new( + db: Arc>, + buffer_size: usize, + sort_method: PackageSort, + filter: PackageFilter, + ) -> Self { + Self { + db, + sort_method, + filter, + state: IterationState::default(), + buffer: Vec::with_capacity(buffer_size), + buffer_index: 0, + buffer_size, + shard_index: 0, + shard_count: 0, + repo_name: None, + } + } + + fn fetch_next_batch(&mut self) -> SoarResult { + let db = self.db.clone(); + let conn = db.lock().map_err(|_| SoarError::PoisonError)?; + + if self.shard_count == 0 { + self.initialize_shards(&conn)?; + } + + if self.shard_index >= self.shard_count { + return Ok(false); + } + + let current_shard = self.get_current_shard(&conn)?; + + if self.should_skip_shard(&conn, ¤t_shard)? { + self.shard_index += 1; + return Ok(true); + } + + self.fetch_packages(&conn, ¤t_shard) + } + + fn initialize_shards(&mut self, conn: &Connection) -> SoarResult<()> { + let mut stmt = conn.prepare("PRAGMA database_list")?; + let rows = stmt.query_map([], |row| row.get::<_, String>(1))?; + self.shard_count = rows.count(); + Ok(()) + } + + fn get_current_shard(&self, conn: &Connection) -> SoarResult { + let mut stmt = conn.prepare("PRAGMA database_list")?; + let mut rows = stmt.query_map([], |row| row.get::<_, String>(1))?; + + let shard = rows + .nth(self.shard_index) + .ok_or(SoarError::DatabaseError(format!( + "Shard index {} out of range", + self.shard_index + )))?; + + Ok(shard?) + } + + fn should_skip_shard(&mut self, conn: &Connection, shard: &str) -> SoarResult { + if self.repo_name.is_none() { + let repo_name = self.get_repository_name(conn, shard)?; + if let Some(ref filter_repo) = self.filter.repo_name { + if filter_repo != &repo_name { + return Ok(true); + } + } + self.repo_name = Some(repo_name); + } + Ok(false) + } + + fn fetch_packages(&mut self, conn: &Connection, shard: &str) -> SoarResult { + let query_builder = QueryBuilder::new( + shard.to_string(), + self.sort_method, + self.filter.clone(), + self.state.clone(), + self.buffer_size, + ); + + let (query, params) = query_builder.build(); + let mut stmt = conn.prepare(&query)?; + + self.buffer.clear(); + self.buffer_index = 0; + + let params_ref: Vec<&dyn rusqlite::ToSql> = params + .iter() + .map(|p| p.as_ref() as &dyn rusqlite::ToSql) + .collect(); + + self.buffer = stmt + .query_map(params_ref.as_slice(), |row| { + map_package(row, self.repo_name.clone().unwrap()) + })? + .filter_map(|res| match res { + Ok(pkg) => Some(pkg), + Err(_) => None, + }) + .collect(); + + self.update_state(); + + if self.buffer.is_empty() { + self.advance_to_next_shard(); + } + + Ok(true) + } + + fn update_state(&mut self) { + if let Some(last_package) = self.buffer.last() { + self.state = IterationState { + id: last_package.id, + pkg_name: Some(last_package.pkg.clone()), + family: Some(last_package.family.clone()), + }; + } + } + + fn advance_to_next_shard(&mut self) { + self.shard_index += 1; + self.state = IterationState::default(); + self.repo_name = None; + } + + fn get_repository_name(&self, conn: &Connection, shard_name: &str) -> SoarResult { + let query = format!("SELECT name FROM {0}.repository LIMIT 1", shard_name); + let mut stmt = conn.prepare(&query)?; + let repo_name: String = stmt.query_row([], |row| row.get(0))?; + Ok(repo_name) + } +} + +impl Iterator for PackageIterator { + type Item = SoarResult; + + fn next(&mut self) -> Option { + if self.buffer_index >= self.buffer.len() { + match self.fetch_next_batch() { + Ok(true) => {} + Ok(false) => return None, + Err(err) => return Some(Err(err)), + } + } + + if self.buffer_index < self.buffer.len() { + let package = self.buffer[self.buffer_index].clone(); + self.buffer_index += 1; + Some(Ok(package)) + } else if self.shard_index < self.shard_count { + self.next() + } else { + None + } + } +} + +pub fn get_all_packages( + db: Arc>, + buffer_size: usize, +) -> SoarResult>> { + Ok(PackageIterator::new( + db, + buffer_size, + PackageSort::Id, + PackageFilter::default(), + )) +} + +pub fn get_packages_with_sort( + db: Arc>, + buffer_size: usize, + sort_method: PackageSort, +) -> SoarResult>> { + Ok(PackageIterator::new( + db, + buffer_size, + sort_method, + PackageFilter::default(), + )) +} + +pub fn get_packages_with_filter( + db: Arc>, + buffer_size: usize, + filter: PackageFilter, +) -> SoarResult>> { + Ok(PackageIterator::new( + db, + buffer_size, + PackageSort::Id, + filter, + )) +} + +pub fn get_packages_with_sort_and_filter( + db: Arc>, + buffer_size: usize, + sort_method: PackageSort, + filter: PackageFilter, +) -> SoarResult>> { + Ok(PackageIterator::new(db, buffer_size, sort_method, filter)) +} + +#[derive(Debug)] +pub struct InstalledPackageIterator { + db: Arc>, + filter: PackageFilter, + state: IterationState, + buffer: Vec, + buffer_index: usize, + buffer_size: usize, +} + +impl InstalledPackageIterator { + pub fn new(db: Arc>, buffer_size: usize, filter: PackageFilter) -> Self { + Self { + db, + filter, + state: IterationState::default(), + buffer: Vec::with_capacity(buffer_size), + buffer_index: 0, + buffer_size, + } + } + + fn fetch_next_batch(&mut self) -> SoarResult { + let db = self.db.clone(); + let conn = db.lock().map_err(|_| SoarError::PoisonError)?; + + self.fetch_packages(&conn) + } + + fn fetch_packages(&mut self, conn: &Connection) -> SoarResult { + let query_builder = InstalledQueryBuilder::new( + PackageSort::Id, + self.filter.clone(), + self.state.clone(), + self.buffer_size, + ); + + let (query, params) = query_builder.build(); + let mut stmt = conn.prepare(&query)?; + + self.buffer.clear(); + self.buffer_index = 0; + + let params_ref: Vec<&dyn rusqlite::ToSql> = params + .iter() + .map(|p| p.as_ref() as &dyn rusqlite::ToSql) + .collect(); + + self.buffer = stmt + .query_map(params_ref.as_slice(), map_installed_package)? + .filter_map(|res| match res { + Ok(pkg) => Some(pkg), + Err(_) => None, + }) + .collect(); + + self.update_state(); + + Ok(!self.buffer.is_empty()) + } + + fn update_state(&mut self) { + if let Some(last_package) = self.buffer.last() { + self.state = IterationState { + id: last_package.id, + pkg_name: Some(last_package.pkg.clone()), + family: Some(last_package.family.clone()), + }; + } + } +} + +impl Iterator for InstalledPackageIterator { + type Item = SoarResult; + + fn next(&mut self) -> Option { + if self.buffer_index >= self.buffer.len() { + match self.fetch_next_batch() { + Ok(true) => {} + Ok(false) => return None, + Err(err) => return Some(Err(err)), + } + } + + if self.buffer_index < self.buffer.len() { + let package = self.buffer[self.buffer_index].clone(); + self.buffer_index += 1; + Some(Ok(package)) + } else { + None + } + } +} + +pub fn get_installed_packages( + db: Arc>, + buffer_size: usize, +) -> SoarResult>> { + Ok(InstalledPackageIterator::new( + db, + buffer_size, + PackageFilter::default(), + )) +} + +pub fn get_installed_packages_with_filter( + db: Arc>, + buffer_size: usize, + filter: PackageFilter, +) -> SoarResult>> { + Ok(InstalledPackageIterator::new(db, buffer_size, filter)) +} + +fn map_package(row: &Row, repo_name: String) -> rusqlite::Result { + Ok(Package { + repo_name, + id: row.get(0)?, + collection: row.get(1)?, + pkg: row.get(2)?, + pkg_id: row.get(3)?, + pkg_name: row.get(4)?, + app_id: row.get(5)?, + family: row.get(6)?, + description: row.get(7)?, + version: row.get(8)?, + size: row.get(9)?, + checksum: row.get(10)?, + note: row.get(11)?, + download_url: row.get(12)?, + build_date: row.get(13)?, + build_script: row.get(14)?, + build_log: row.get(15)?, + homepage: row.get(16)?, + category: row.get(17)?, + source_url: row.get(18)?, + icon: row.get(19)?, + desktop: row.get(20)?, + }) +} + +pub fn map_installed_package(row: &Row) -> rusqlite::Result { + Ok(InstalledPackage { + id: row.get(0)?, + repo_name: row.get(1)?, + collection: row.get(2)?, + family: row.get(3)?, + pkg_name: row.get(4)?, + pkg: row.get(5)?, + pkg_id: row.get(6)?, + app_id: row.get(7)?, + description: row.get(8)?, + version: row.get(9)?, + size: row.get(10)?, + checksum: row.get(11)?, + build_date: row.get(12)?, + build_script: row.get(13)?, + build_log: row.get(14)?, + category: row.get(15)?, + bin_path: row.get(16)?, + installed_path: row.get(17)?, + installed_date: row.get(18)?, + disabled: row.get(19)?, + pinned: row.get(20)?, + is_installed: row.get(21)?, + installed_with_family: row.get(22)?, + }) +} diff --git a/soar-core/src/database/packages/mod.rs b/soar-core/src/database/packages/mod.rs new file mode 100644 index 0000000..30e731f --- /dev/null +++ b/soar-core/src/database/packages/mod.rs @@ -0,0 +1,7 @@ +mod iterator; +mod models; +mod query; + +pub use iterator::*; +pub use models::*; +pub use query::*; diff --git a/soar-core/src/database/packages/models.rs b/soar-core/src/database/packages/models.rs new file mode 100644 index 0000000..c151b39 --- /dev/null +++ b/soar-core/src/database/packages/models.rs @@ -0,0 +1,74 @@ +#[derive(Debug, Clone, Copy)] +pub enum PackageSort { + Id, + PackageName, + Family, + FamilyAndPackage, +} + +impl PackageSort { + pub fn get_order_clause(&self) -> &'static str { + match self { + PackageSort::Id => "p.id", + PackageSort::PackageName => "p.pkg_name, p.id", + PackageSort::Family => "f.name, p.id", + PackageSort::FamilyAndPackage => "f.name, p.pkg_name, p.id", + } + } + + pub fn get_next_page_condition(&self) -> &'static str { + match self { + PackageSort::Id => "p.id > ?", + PackageSort::PackageName => "(p.pkg_name, p.id) > (?, ?)", + PackageSort::Family => "(f.name, p.id) > (?, ?)", + PackageSort::FamilyAndPackage => "(f.name, p.pkg_name, p.id) > (?, ?, ?)", + } + } + + pub fn bind_pagination_params( + &self, + params: &mut Vec>, + state: &IterationState, + ) { + match self { + Self::Id => { + params.push(Box::new(state.id)); + } + Self::PackageName => { + let pkg_name = state.pkg_name.clone().unwrap_or_default(); + params.push(Box::new(pkg_name)); + params.push(Box::new(state.id)); + } + Self::Family => { + let family = state.family.clone().unwrap_or_default(); + params.push(Box::new(family)); + params.push(Box::new(state.id)); + } + Self::FamilyAndPackage => { + let family = state.family.clone().unwrap_or_default(); + let pkg_name = state.pkg_name.clone().unwrap_or_default(); + params.push(Box::new(family)); + params.push(Box::new(pkg_name)); + params.push(Box::new(state.id)); + } + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct PackageFilter { + pub repo_name: Option, + pub collection: Option, + pub pkg_name: Option, + pub exact_pkg_name: Option, + pub family: Option, + pub search_term: Option, + pub exact_case: bool, +} + +#[derive(Debug, Default, Clone)] +pub struct IterationState { + pub id: u64, + pub pkg_name: Option, + pub family: Option, +} diff --git a/soar-core/src/database/packages/query.rs b/soar-core/src/database/packages/query.rs new file mode 100644 index 0000000..9a72358 --- /dev/null +++ b/soar-core/src/database/packages/query.rs @@ -0,0 +1,222 @@ +use rusqlite::ToSql; + +use super::models::{IterationState, PackageFilter, PackageSort}; + +pub struct QueryBuilder { + shard_name: String, + sort_method: PackageSort, + filter: PackageFilter, + state: IterationState, + buffer_size: usize, +} + +impl QueryBuilder { + pub fn new( + shard_name: String, + sort_method: PackageSort, + filter: PackageFilter, + state: IterationState, + buffer_size: usize, + ) -> Self { + Self { + shard_name, + sort_method, + filter, + state, + buffer_size, + } + } + + pub fn build(&self) -> (String, Vec>) { + let (where_clause, filter_params) = self.build_filter_clause(); + let query = self.build_query_string(&where_clause); + + let mut params: Vec> = filter_params + .into_iter() + .map(|s| Box::new(s) as Box) + .collect(); + + self.sort_method + .bind_pagination_params(&mut params, &self.state); + params.push(Box::new(self.buffer_size)); + + (query, params) + } + + fn build_filter_clause(&self) -> (String, Vec) { + let mut conditions = Vec::new(); + let mut params = Vec::new(); + + if let Some(collection) = &self.filter.collection { + conditions.push("c.name = ?".to_string()); + params.push(collection.clone()); + } + + if let Some(pkg_name) = &self.filter.pkg_name { + conditions.push("p.pkg_name LIKE ?".to_string()); + params.push(format!("%{}%", pkg_name)); + } + + if let Some(pkg_name) = &self.filter.exact_pkg_name { + conditions.push("p.pkg_name = ?".to_string()); + params.push(pkg_name.clone()); + } + + if let Some(family) = &self.filter.family { + conditions.push("f.name = ?".to_string()); + params.push(family.clone()); + } + + if let Some(search) = &self.filter.search_term { + conditions.push("(p.pkg_name LIKE ? OR p.description LIKE ?)".to_string()); + params.push(format!("%{}%", search)); + params.push(format!("%{}%", search)); + } + + let where_clause = if conditions.is_empty() { + "1=1".to_string() + } else { + conditions.join(" AND ") + }; + + (where_clause, params) + } + + fn build_query_string(&self, where_clause: &str) -> String { + format!( + r#" + SELECT + p.id, c.name AS collection, p.pkg, p.pkg_id, p.pkg_name, + p.app_id, f.name AS family, p.description, p.version, + p.size, p.checksum, n.note AS note, p.download_url, + p.build_date, p.build_script, p.build_log, h.url AS homepage, + p.category, su.url AS source_url, i.url AS icon, p.desktop + FROM + {0}.packages p + LEFT JOIN {0}.collections c ON c.id = p.collection_id + LEFT JOIN {0}.families f ON f.id = p.family_id + LEFT JOIN {0}.icons i ON i.id = p.icon_id + LEFT JOIN {0}.homepages h ON h.package_id = p.id + LEFT JOIN {0}.notes n ON n.package_id = p.id + LEFT JOIN {0}.source_urls su ON su.package_id = p.id + WHERE {1} AND {2} + ORDER BY {3} + LIMIT ? + {4} + "#, + self.shard_name, + where_clause, + self.sort_method.get_next_page_condition(), + self.sort_method.get_order_clause(), + if self.filter.exact_case { + "COLLATE BINARY" + } else { + "" + } + ) + } +} + +pub struct InstalledQueryBuilder { + filter: PackageFilter, + sort_method: PackageSort, + state: IterationState, + buffer_size: usize, +} + +impl InstalledQueryBuilder { + pub fn new( + sort_method: PackageSort, + filter: PackageFilter, + state: IterationState, + buffer_size: usize, + ) -> Self { + Self { + sort_method, + filter, + state, + buffer_size, + } + } + + pub fn build(&self) -> (String, Vec>) { + let (where_clause, filter_params) = self.build_filter_clause(); + let query = self.build_query_string(&where_clause); + + let mut params: Vec> = filter_params + .into_iter() + .map(|s| Box::new(s) as Box) + .collect(); + + self.sort_method + .bind_pagination_params(&mut params, &self.state); + + params.push(Box::new(self.buffer_size)); + + (query, params) + } + + fn build_filter_clause(&self) -> (String, Vec) { + let mut conditions = Vec::new(); + let mut params = Vec::new(); + + if let Some(collection) = &self.filter.collection { + conditions.push("collection = ?".to_string()); + params.push(collection.clone()); + } + + if let Some(pkg_name) = &self.filter.pkg_name { + conditions.push("pkg_name LIKE ?".to_string()); + params.push(format!("%{}%", pkg_name)); + } + + if let Some(pkg_name) = &self.filter.exact_pkg_name { + conditions.push("pkg_name = ?".to_string()); + params.push(pkg_name.clone()); + } + + if let Some(family) = &self.filter.family { + conditions.push("family = ?".to_string()); + params.push(family.clone()); + } + + if let Some(search) = &self.filter.search_term { + conditions.push("(pkg_name LIKE ? OR description LIKE ?)".to_string()); + params.push(format!("%{}%", search)); + params.push(format!("%{}%", search)); + } + + let where_clause = if conditions.is_empty() { + "1=1".to_string() + } else { + conditions.join(" AND ") + }; + + (where_clause, params) + } + + fn build_query_string(&self, where_clause: &str) -> String { + format!( + r#" + SELECT + id, repo_name, collection, family, pkg_name, pkg, + pkg_id, app_id, description, version, size, checksum, + build_date, build_script, build_log, category, bin_path, + installed_path, installed_date, disabled, pinned, + is_installed, installed_with_family + FROM + packages p + WHERE {0} AND {1} + LIMIT ? + {2} + "#, + where_clause, + self.sort_method.get_next_page_condition(), + if self.filter.exact_case { + "COLLATE BINARY" + } else { + "" + } + ) + } +} diff --git a/soar-core/src/database/repository.rs b/soar-core/src/database/repository.rs new file mode 100644 index 0000000..fd84479 --- /dev/null +++ b/soar-core/src/database/repository.rs @@ -0,0 +1,111 @@ +use rusqlite::{params, Result, Transaction}; + +use super::{ + models::{RemotePackage, RemotePackageMetadata}, + statements::DbStatements, +}; + +pub struct PackageRepository<'a> { + tx: &'a Transaction<'a>, + statements: DbStatements<'a>, + repo_name: &'a str, +} + +impl<'a> PackageRepository<'a> { + pub fn new(tx: &'a Transaction<'a>, statements: DbStatements<'a>, repo_name: &'a str) -> Self { + Self { + tx, + statements, + repo_name, + } + } + + pub fn import_packages(&mut self, metadata: &RemotePackageMetadata) -> Result<()> { + self.get_or_create_repo(self.repo_name)?; + + for (col_name, packages) in &metadata.collection { + let collection_id = self.get_or_create_collection(col_name)?; + + for package in packages { + self.insert_package(package, collection_id)?; + } + } + Ok(()) + } + + fn get_or_create_repo(&mut self, name: &str) -> Result<()> { + self.statements + .repo_check + .query_row([], |_| Ok(())) + .or_else(|_| { + self.statements.repo_insert.execute(params![name])?; + Ok(()) + }) + } + + fn get_or_create_collection(&mut self, name: &str) -> Result { + self.statements + .collection_check + .query_row(params![name], |row| row.get(0)) + .or_else(|_| { + self.statements.collection_insert.execute(params![name])?; + Ok(self.tx.last_insert_rowid()) + }) + } + + fn get_or_create_icon(&mut self, url: &str) -> Result { + self.statements + .icon_check + .query_row(params![url], |row| row.get(0)) + .or_else(|_| { + self.statements.icon_insert.execute(params![url])?; + Ok(self.tx.last_insert_rowid()) + }) + } + + fn insert_package(&mut self, package: &RemotePackage, collection_id: i64) -> Result<()> { + // FIXME: need to check provides, and deal with family appropriately + // currently, it creates new family for each package + self.statements + .family_insert + .execute(params![package.pkg_family.clone().unwrap_or_default()])?; + let family_id = self.tx.last_insert_rowid(); + let icon_id = self.get_or_create_icon(&package.icon)?; + + self.statements.package_insert.execute(params![ + package.pkg, + package.pkg_name, + package.pkg_id, + package.description, + package.version, + package.download_url, + package.size, + package.bsum, + package.build_date, + package.build_script, + package.build_log, + package.category, + package.desktop, + family_id, + icon_id, + collection_id + ])?; + + let package_id = self.tx.last_insert_rowid(); + + self.statements + .homepage_insert + .execute(params![package.homepage, package_id])?; + self.statements + .note_insert + .execute(params![package.note, package_id])?; + self.statements + .source_url_insert + .execute(params![package.src_url, package_id])?; + self.statements + .provides_insert + .execute(params![family_id, package_id])?; + + Ok(()) + } +} diff --git a/soar-core/src/database/statements.rs b/soar-core/src/database/statements.rs new file mode 100644 index 0000000..e81d7bc --- /dev/null +++ b/soar-core/src/database/statements.rs @@ -0,0 +1,44 @@ +use rusqlite::{Statement, Transaction}; + +pub struct DbStatements<'a> { + pub repo_insert: Statement<'a>, + pub repo_check: Statement<'a>, + pub collection_insert: Statement<'a>, + pub collection_check: Statement<'a>, + pub family_insert: Statement<'a>, + pub homepage_insert: Statement<'a>, + pub note_insert: Statement<'a>, + pub source_url_insert: Statement<'a>, + pub icon_insert: Statement<'a>, + pub icon_check: Statement<'a>, + pub provides_insert: Statement<'a>, + pub package_insert: Statement<'a>, +} + +impl<'a> DbStatements<'a> { + pub fn new(tx: &'a Transaction) -> rusqlite::Result { + Ok(Self { + repo_insert: tx.prepare("INSERT INTO repository (name) VALUES (?1)")?, + repo_check: tx.prepare("SELECT name FROM repository LIMIT 1")?, + collection_insert: tx.prepare("INSERT INTO collections (name) VALUES (?1)")?, + collection_check: tx.prepare("SELECT id FROM collections WHERE name = ?1")?, + family_insert: tx.prepare("INSERT INTO families (name) Values (?1)")?, + homepage_insert: tx + .prepare("INSERT INTO homepages (url, package_id) Values (?1, ?2)")?, + note_insert: tx.prepare("INSERT INTO notes (note, package_id) Values (?1, ?2)")?, + source_url_insert: tx + .prepare("INSERT INTO source_urls (url, package_id) Values (?1, ?2)")?, + icon_insert: tx.prepare("INSERT INTO icons (url) Values (?1)")?, + icon_check: tx.prepare("SELECT id FROM icons WHERE url = ?1")?, + provides_insert: tx + .prepare("INSERT INTO provides (family_id, package_id) Values (?1, ?2)")?, + package_insert: tx.prepare( + "INSERT INTO packages ( + pkg, pkg_name, pkg_id, description, version, download_url, size, + checksum, build_date, build_script, build_log, category, + desktop, family_id, icon_id, collection_id + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)", + )?, + }) + } +} diff --git a/soar-core/src/error.rs b/soar-core/src/error.rs new file mode 100644 index 0000000..79739b4 --- /dev/null +++ b/soar-core/src/error.rs @@ -0,0 +1,110 @@ +use std::error::Error; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SoarError { + #[error("System error: {0}")] + Errno(#[from] nix::errno::Errno), + + #[error("Environment variable error: {0}")] + VarError(#[from] std::env::VarError), + + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("System time error: {0}")] + SystemTimeError(#[from] std::time::SystemTimeError), + + #[error("TOML serialization error: {0}")] + TomlError(#[from] toml::ser::Error), + + #[error("SQLite database error: {0}")] + RusqliteError(#[from] rusqlite::Error), + + #[error("Database operation failed: {0}")] + DatabaseError(String), + + #[error("HTTP request error: {0}")] + ReqwestError(#[from] reqwest::Error), + + #[error("Download failed: {0}")] + DownloadError(#[from] soar_dl::error::DownloadError), + + #[error("{0}")] + PlatformError(soar_dl::error::PlatformError), + + #[error("Squashy Error: {0}")] + SquishyError(#[from] squishy::error::SquishyError), + + #[error("Image Error: {0}")] + ImageError(#[from] image::error::ImageError), + + #[error("Package integration failed: {0}")] + PackageIntegrationFailed(String), + + #[error("Package {0} not found")] + PackageNotFound(String), + + #[error("Failed to fetch from remote source")] + FailedToFetchRemote, + + #[error("Invalid path specified")] + InvalidPath, + + #[error("Thread lock poison error")] + PoisonError, + + #[error("Invalid checksum detected")] + InvalidChecksum, + + #[error("Invalid configuration")] + InvalidConfig, + + #[error("Configuration file already exists")] + ConfigAlreadyExists, + + #[error("Invalid package query: {0}")] + InvalidPackageQuery(String), + + #[error("{0}")] + Custom(String), +} + +impl SoarError { + pub fn message(&self) -> String { + self.to_string() + } + + pub fn root_cause(&self) -> String { + match self { + Self::IoError(e) => format!( + "Root cause: {}", + e.source() + .map_or_else(|| e.to_string(), |source| source.to_string()) + ), + Self::ReqwestError(e) => format!( + "Root cause: {}", + e.source() + .map_or_else(|| e.to_string(), |source| source.to_string()) + ), + Self::RusqliteError(e) => format!( + "Root cause: {}", + e.source() + .map_or_else(|| e.to_string(), |source| source.to_string()) + ), + _ => self.to_string(), + } + } +} + +impl From> for SoarError { + fn from(_: std::sync::PoisonError) -> Self { + Self::PoisonError + } +} + +impl From for SoarError { + fn from(value: soar_dl::error::PlatformError) -> Self { + Self::PlatformError(value) + } +} diff --git a/soar-core/src/lib.rs b/soar-core/src/lib.rs new file mode 100644 index 0000000..157bc65 --- /dev/null +++ b/soar-core/src/lib.rs @@ -0,0 +1,11 @@ +use error::SoarError; + +pub mod config; +pub mod constants; +pub mod database; +pub mod error; +pub mod metadata; +pub mod package; +pub mod utils; + +pub type SoarResult = std::result::Result; diff --git a/soar-core/src/metadata.rs b/soar-core/src/metadata.rs new file mode 100644 index 0000000..ba67106 --- /dev/null +++ b/soar-core/src/metadata.rs @@ -0,0 +1,52 @@ +use std::fs::{self, File}; + +use crate::{ + config::Repository, + database::{connection::Database, models::RemotePackageMetadata}, + error::SoarError, + SoarResult, +}; + +pub async fn fetch_metadata(repo: Repository) -> SoarResult<()> { + let repo_path = repo.get_path(); + let remote_url = format!( + "{}/{}", + repo.url, + repo.metadata.unwrap_or("metadata.json".into()) + ); + if !repo_path.is_dir() { + return Err(SoarError::InvalidPath); + } + + let checksum_file = repo_path.join("metadata.bsum"); + let remote_checksum_url = format!("{}.bsum", remote_url); + let resp = reqwest::get(&remote_checksum_url).await?; + if !resp.status().is_success() { + return Err(SoarError::FailedToFetchRemote); + } + let remote_checksum = resp.text().await?; + if let Ok(checksum) = fs::read_to_string(&checksum_file) { + if checksum == remote_checksum { + return Ok(()); + } + } + + let metadata_db = repo_path.join("metadata.db"); + + let _ = fs::remove_file(&metadata_db); + File::create(&metadata_db)?; + soar_db::metadata::init_db(&metadata_db).unwrap(); + + let resp = reqwest::get(&remote_url).await?; + if !resp.status().is_success() { + return Err(SoarError::FailedToFetchRemote); + } + let remote_metadata: RemotePackageMetadata = resp.json().await?; + + let db = Database::new(metadata_db)?; + db.from_json_metadata(remote_metadata, &repo.name)?; + + fs::write(checksum_file, remote_checksum)?; + + Ok(()) +} diff --git a/soar-core/src/package/formats/appimage.rs b/soar-core/src/package/formats/appimage.rs new file mode 100644 index 0000000..63475b9 --- /dev/null +++ b/soar-core/src/package/formats/appimage.rs @@ -0,0 +1,60 @@ +use std::{fs, path::Path}; + +use squishy::{appimage::AppImage, EntryKind}; + +use crate::{ + constants::PNG_MAGIC_BYTES, database::models::Package, utils::calc_magic_bytes, SoarResult, +}; + +use super::common::{symlink_desktop, symlink_icon}; + +pub async fn integrate_appimage>(file_path: P, package: &Package) -> SoarResult<()> { + let appimage = AppImage::new(None, &file_path, None)?; + let squashfs = &appimage.squashfs; + + if let Some(entry) = appimage.find_icon() { + if let EntryKind::File(basic_file) = entry.kind { + let dest = format!("{}.DirIcon", package.pkg_name); + let _ = squashfs.write_file(basic_file, &dest); + + let magic_bytes = calc_magic_bytes(&dest, 8)?; + let ext = if magic_bytes == PNG_MAGIC_BYTES { + "png" + } else { + "svg" + }; + let final_path = format!("{}.{ext}", package.pkg_name); + fs::rename(&dest, &final_path)?; + + symlink_icon(final_path, &package.pkg_name).await?; + } + } + + if let Some(entry) = appimage.find_desktop() { + if let EntryKind::File(basic_file) = entry.kind { + let dest = format!("{}.desktop", package.pkg_name); + let _ = squashfs.write_file(basic_file, &dest); + + symlink_desktop(dest, &package).await?; + } + } + + if let Some(entry) = appimage.find_appstream() { + if let EntryKind::File(basic_file) = entry.kind { + let file_name = if entry + .path + .file_name() + .unwrap() + .to_string_lossy() + .contains("appdata") + { + "appdata" + } else { + "metainfo" + }; + let dest = format!("{}.{file_name}.xml", package.pkg_name); + let _ = squashfs.write_file(basic_file, &dest); + } + } + Ok(()) +} diff --git a/soar-core/src/package/formats/common.rs b/soar-core/src/package/formats/common.rs new file mode 100644 index 0000000..c198be3 --- /dev/null +++ b/soar-core/src/package/formats/common.rs @@ -0,0 +1,256 @@ +use std::{ + fs::{self, File}, + io::{BufReader, BufWriter, Write}, + path::{Path, PathBuf}, +}; + +use futures::try_join; +use image::{imageops::FilterType, DynamicImage, GenericImageView}; +use regex::Regex; +use soar_dl::downloader::{DownloadOptions, Downloader}; + +use crate::{ + constants::{bin_path, PNG_MAGIC_BYTES}, + database::models::Package, + utils::{calc_magic_bytes, create_symlink, home_data_path}, + SoarResult, +}; + +use super::{appimage::integrate_appimage, get_file_type}; + +const SUPPORTED_DIMENSIONS: &[(u32, u32)] = &[ + (16, 16), + (24, 24), + (32, 32), + (48, 48), + (64, 64), + (72, 72), + (80, 80), + (96, 96), + (128, 128), + (192, 192), + (256, 256), + (512, 512), +]; + +fn find_nearest_supported_dimension(width: u32, height: u32) -> (u32, u32) { + SUPPORTED_DIMENSIONS + .iter() + .min_by_key(|&&(w, h)| { + let width_diff = (w as i32 - width as i32).abs(); + let height_diff = (h as i32 - height as i32).abs(); + width_diff + height_diff + }) + .cloned() + .unwrap_or((width, height)) +} + +fn normalize_image(image: DynamicImage) -> DynamicImage { + let (width, height) = image.dimensions(); + let (new_width, new_height) = find_nearest_supported_dimension(width, height); + + if (width, height) != (new_width, new_height) { + image.resize(new_width, new_height, FilterType::Lanczos3) + } else { + image + } +} + +pub async fn symlink_icon>(real_path: P, pkg_name: &str) -> SoarResult<()> { + let real_path = real_path.as_ref(); + let image = image::open(real_path)?; + let (orig_w, orig_h) = image.dimensions(); + + let normalized_image = normalize_image(image); + let (w, h) = normalized_image.dimensions(); + + if (w, h) != (orig_w, orig_h) { + normalized_image.save(real_path)?; + } + + let ext = real_path.extension().unwrap_or_default(); + let final_path = PathBuf::from(format!( + "{}/icons/hicolor/{w}x{h}/apps/{pkg_name}-soar.{ext:#?}", + home_data_path() + )); + + if let Some(parent) = final_path.parent() { + fs::create_dir_all(parent)?; + } + + create_symlink(real_path, &final_path) +} + +pub async fn symlink_desktop>(real_path: P, package: &Package) -> SoarResult<()> { + let real_path = real_path.as_ref(); + let content = fs::read_to_string(real_path)?; + + let final_content = { + let re = Regex::new(r"(?m)^(Icon|Exec|TryExec)=(.*)").unwrap(); + + re.replace_all(&content, |caps: ®ex::Captures| match &caps[1] { + "Icon" => format!("Icon={}", package.pkg), + "Exec" | "TryExec" => { + format!("{}={}/{}", &caps[1], bin_path().display(), package.pkg) + } + _ => unreachable!(), + }) + .to_string() + }; + + let mut writer = BufWriter::new(File::create(real_path)?); + writer.write_all(final_content.as_bytes())?; + + let final_path = PathBuf::from(format!( + "{}/applications/{}-soar.desktop", + home_data_path(), + package.pkg_name + )); + + create_symlink(real_path, &final_path) +} + +pub async fn integrate_remote>( + package_path: P, + package: &Package, +) -> SoarResult<()> { + let package_path = package_path.as_ref(); + let icon_url = &package.icon; + let desktop_url = &package.desktop; + + let mut icon_output_path = package_path.join(".DirIcon"); + let desktop_output_path = package_path.join(format!("{}.desktop", package.pkg)); + + let downloader = Downloader::default(); + + if let Some(icon_url) = icon_url { + let options = DownloadOptions { + url: icon_url.clone(), + output_path: Some(icon_output_path.to_string_lossy().to_string()), + progress_callback: None, + }; + downloader.download(options).await?; + + let ext = if calc_magic_bytes(icon_output_path, 8)? == PNG_MAGIC_BYTES { + "png" + } else { + "svg" + }; + icon_output_path = package_path.join(format!("{}.{}", package.pkg, ext)); + } + + if let Some(desktop_url) = desktop_url { + let options = DownloadOptions { + url: desktop_url.clone(), + output_path: Some(desktop_output_path.to_string_lossy().to_string()), + progress_callback: None, + }; + downloader.download(options).await?; + } else { + let content = create_default_desktop_entry( + &package.pkg, + &package.pkg_name, + &package.category.replace(',', ";"), + ); + fs::write(&desktop_output_path, &content)?; + } + + try_join!( + symlink_icon(&icon_output_path, &package.pkg_name), + symlink_desktop(&desktop_output_path, &package) + )?; + + Ok(()) +} + +pub fn setup_portable_dir>( + package_path: P, + package: &Package, + portable: Option, + portable_home: Option, + portable_config: Option, +) -> SoarResult<()> { + let package_path = package_path.as_ref(); + + let pkg_config = package_path.with_extension("config"); + let pkg_home = package_path.with_extension("home"); + + let (portable_home, portable_config) = if let Some(portable) = portable { + (Some(portable.clone()), Some(portable.clone())) + } else { + (portable_home, portable_config) + }; + + if let Some(portable_home) = portable_home { + if portable_home.is_empty() { + fs::create_dir(&pkg_home)?; + } else { + let portable_home = PathBuf::from(portable_home) + .join(&package.pkg_name) + .with_extension("home"); + fs::create_dir_all(&portable_home)?; + create_symlink(&portable_home, &pkg_home)?; + } + } + + if let Some(portable_config) = portable_config { + if portable_config.is_empty() { + fs::create_dir(&pkg_config)?; + } else { + let portable_config = PathBuf::from(portable_config) + .join(&package.pkg_name) + .with_extension("config"); + fs::create_dir_all(&portable_config)?; + create_symlink(&portable_config, &pkg_config)?; + } + } + + Ok(()) +} + +fn create_default_desktop_entry(bin_name: &str, name: &str, categories: &str) -> Vec { + format!( + "[Desktop Entry]\n\ + Type=Application\n\ + Name={}\n\ + Icon={}\n\ + Exec={}\n\ + Categories={};\n", + name, bin_name, bin_name, categories + ) + .as_bytes() + .to_vec() +} + +pub async fn integrate_package>( + package_path: P, + package: &Package, + portable: Option, + portable_home: Option, + portable_config: Option, +) -> SoarResult<()> { + let package_path = package_path.as_ref(); + let bin_path = package_path.join(&package.pkg); + let mut reader = BufReader::new(File::open(&bin_path)?); + let file_type = get_file_type(&mut reader); + + match file_type { + super::PackageFormat::AppImage => { + if integrate_appimage(bin_path, package).await.is_ok() { + setup_portable_dir( + package_path, + package, + portable, + portable_home, + portable_config, + )?; + } + } + super::PackageFormat::FlatImage => { + setup_portable_dir(package_path, package, None, None, portable_config)?; + } + _ => {} + } + + Ok(()) +} diff --git a/src/core/file.rs b/soar-core/src/package/formats/mod.rs similarity index 52% rename from src/core/file.rs rename to soar-core/src/package/formats/mod.rs index d36850a..2cbe679 100644 --- a/src/core/file.rs +++ b/soar-core/src/package/formats/mod.rs @@ -1,30 +1,31 @@ use std::io::{BufReader, Read}; -use super::constant::{APPIMAGE_MAGIC_BYTES, ELF_MAGIC_BYTES, FLATIMAGE_MAGIC_BYTES}; +use crate::constants::{APPIMAGE_MAGIC_BYTES, ELF_MAGIC_BYTES, FLATIMAGE_MAGIC_BYTES}; + +pub mod appimage; +pub mod common; #[derive(PartialEq, Eq)] -pub enum FileType { +pub enum PackageFormat { AppImage, FlatImage, ELF, Unknown, } -pub fn get_file_type(file: &mut BufReader) -> FileType +pub fn get_file_type(file: &mut BufReader) -> PackageFormat where T: Read, { let mut magic_bytes = [0u8; 12]; if file.read_exact(&mut magic_bytes).is_ok() { if magic_bytes[8..] == APPIMAGE_MAGIC_BYTES { - return FileType::AppImage; + return PackageFormat::AppImage; } else if magic_bytes[8..] == FLATIMAGE_MAGIC_BYTES { - return FileType::FlatImage; + return PackageFormat::FlatImage; } else if magic_bytes[..4] == ELF_MAGIC_BYTES { - return FileType::ELF; - } else { - return FileType::Unknown; + return PackageFormat::ELF; } } - FileType::Unknown + PackageFormat::Unknown } diff --git a/soar-core/src/package/install.rs b/soar-core/src/package/install.rs new file mode 100644 index 0000000..1196d83 --- /dev/null +++ b/soar-core/src/package/install.rs @@ -0,0 +1,134 @@ +use std::{ + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; + +use chrono::Utc; +use rusqlite::{params, Connection}; +use soar_dl::downloader::{DownloadOptions, DownloadState, Downloader}; + +use crate::{database::models::Package, utils::validate_checksum, SoarResult}; + +pub struct PackageInstaller { + package: Package, + install_dir: PathBuf, + progress_callback: Option>, + db: Arc>, + installed_with_family: bool, +} + +impl PackageInstaller { + pub async fn new>( + package: Package, + install_dir: P, + progress_callback: Option>, + db: Arc>, + installed_with_family: bool, + ) -> SoarResult { + let install_dir = install_dir.as_ref().to_path_buf(); + { + let conn = db.lock()?; + let mut stmt = conn.prepare( + r#" + INSERT OR IGNORE INTO packages ( + repo_name, collection, family, pkg_name, + pkg, pkg_id, app_id, description, + version, size, checksum, build_date, + build_script, build_log, category, + installed_path, installed_with_family + ) + VALUES + ( + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, + ?11, ?12, ?13, ?14, ?15, ?16, ?17 + ) + "#, + )?; + stmt.execute(params![ + package.repo_name, + package.collection, + package.family, + package.pkg_name, + package.pkg, + package.pkg_id, + package.app_id, + package.description, + package.version, + package.size, + package.checksum, + package.build_date, + package.build_script, + package.build_log, + package.category, + install_dir.to_string_lossy(), + installed_with_family + ])?; + } + Ok(Self { + package, + install_dir, + progress_callback, + db: db.clone(), + installed_with_family, + }) + } + + pub async fn install(&self) -> SoarResult<()> { + let package = &self.package; + let output_path = self.install_dir.join(&package.pkg); + + self.download_package(&output_path).await?; + + validate_checksum(&package.checksum, &output_path)?; + + Ok(()) + } + + async fn download_package>(&self, output_path: P) -> SoarResult<()> { + let downloader = Downloader::default(); + let options = DownloadOptions { + url: self.package.download_url.clone(), + output_path: Some(output_path.as_ref().to_string_lossy().to_string()), + progress_callback: self.progress_callback.clone(), + }; + + downloader.download(options).await?; + + Ok(()) + } + + pub async fn record>( + &self, + final_checksum: &str, + bin_path: P, + ) -> SoarResult<()> { + let conn = self.db.lock()?; + let package = &self.package; + let mut stmt = conn.prepare( + r#" + UPDATE packages + SET + bin_path = ?4, + checksum = ?5, + installed_date = ?6, + is_installed = ?7, + installed_with_family = ?8 + WHERE + family = ?1 AND pkg_name = ?2 AND checksum = ?3 + "#, + )?; + let now = Utc::now().timestamp_millis(); + stmt.execute(params![ + package.family, + package.pkg_name, + package.checksum, + bin_path.as_ref().to_string_lossy(), + final_checksum, + now, + true, + self.installed_with_family + ])?; + + Ok(()) + } +} diff --git a/soar-core/src/package/mod.rs b/soar-core/src/package/mod.rs new file mode 100644 index 0000000..bb8c5da --- /dev/null +++ b/soar-core/src/package/mod.rs @@ -0,0 +1,4 @@ +pub mod formats; +pub mod install; +pub mod query; +pub mod remove; diff --git a/soar-core/src/package/query.rs b/soar-core/src/package/query.rs new file mode 100644 index 0000000..2469db8 --- /dev/null +++ b/soar-core/src/package/query.rs @@ -0,0 +1,75 @@ +use std::sync::OnceLock; + +use regex::Regex; + +use crate::{database::packages::PackageFilter, error::SoarError}; + +#[derive(Debug)] +pub struct PackageQuery { + pub name: String, + pub repo_name: Option, + pub collection: Option, + pub family: Option, + pub version: Option, +} + +impl PackageFilter { + pub fn from_query(query: PackageQuery) -> Self { + PackageFilter { + repo_name: query.repo_name, + collection: query.collection, + exact_pkg_name: Some(query.name), + family: query.family, + ..Default::default() + } + } +} + +impl TryFrom<&str> for PackageQuery { + type Error = SoarError; + + fn try_from(value: &str) -> Result { + static PACKAGE_RE: OnceLock = OnceLock::new(); + let re = PACKAGE_RE.get_or_init(|| { + Regex::new( + r"(?x) + ^(?:(?P[^\/\#\@:]+)/)? # optional family followed by / + (?P[^\/\#\@:]+) # required package name + (?:\#(?P[^@:]+))? # optional collection after # + (?:@(?P[^:]+))? # optional version after @ + (?::(?P[^:]+))?$ # optional repo after : + ", + ) + .unwrap() + }); + + let query = value.trim().to_lowercase(); + if query.is_empty() { + return Err(SoarError::InvalidPackageQuery( + "Package query can't be empty".into(), + )); + } + + let caps = re.captures(&query).ok_or(SoarError::InvalidPackageQuery( + "Invalid package query format".into(), + ))?; + + let name = caps.name("name").map(|m| m.as_str().to_string()).ok_or( + SoarError::InvalidPackageQuery("Package name is required".into()), + )?; + + if name.is_empty() { + return Err(SoarError::InvalidPackageQuery( + "Package name cannot be empty".into(), + )); + } + + Ok(PackageQuery { + repo_name: caps.name("repo").map(|m| m.as_str().to_string()), + collection: caps.name("collection").map(|m| m.as_str().to_string()), + family: caps.name("family").map(|m| m.as_str().to_string()), + name, + version: caps.name("version").map(|m| m.as_str().to_string()), + }) + } +} diff --git a/soar-core/src/package/remove.rs b/soar-core/src/package/remove.rs new file mode 100644 index 0000000..7f9f234 --- /dev/null +++ b/soar-core/src/package/remove.rs @@ -0,0 +1,36 @@ +use std::{ + fs, + sync::{Arc, Mutex}, +}; + +use rusqlite::{params, Connection}; + +use crate::{database::models::InstalledPackage, SoarResult}; + +pub struct PackageRemover { + package: InstalledPackage, + db: Arc>, +} + +impl PackageRemover { + pub async fn new(package: InstalledPackage, db: Arc>) -> Self { + Self { package, db } + } + + pub async fn remove(&self) -> SoarResult<()> { + let conn = self.db.lock()?; + let mut stmt = conn.prepare( + r#" + DELETE FROM packages WHERE id = ? AND is_installed = true + "#, + )?; + + // if the package is installed, it does have bin_path + fs::remove_file(&self.package.bin_path.clone().unwrap())?; + fs::remove_dir_all(&self.package.installed_path)?; + + stmt.execute(params![self.package.id])?; + + Ok(()) + } +} diff --git a/soar-core/src/package/update.rs b/soar-core/src/package/update.rs new file mode 100644 index 0000000..e69de29 diff --git a/soar-core/src/utils.rs b/soar-core/src/utils.rs new file mode 100644 index 0000000..dfb019f --- /dev/null +++ b/soar-core/src/utils.rs @@ -0,0 +1,197 @@ +use std::{ + env, + fs::{self, File}, + io::{BufReader, Read, Seek}, + os, + path::{Path, PathBuf}, +}; + +use nix::unistd::{geteuid, User}; + +use crate::{ + constants::{bin_path, cache_path, db_path, packages_path}, + error::SoarError, + SoarResult, +}; + +type Result = std::result::Result; + +fn get_username() -> Result { + let uid = geteuid(); + User::from_uid(uid)? + .ok_or_else(|| panic!("Failed to get user")) + .map(|user| user.name) +} + +pub fn home_path() -> String { + env::var("HOME").unwrap_or_else(|_| { + let username = env::var("USER") + .or_else(|_| env::var("LOGNAME")) + .or_else(|_| get_username().map_err(|_| ())) + .unwrap_or_else(|_| panic!("Couldn't determine username. Please fix the system.")); + format!("/home/{}", username) + }) +} + +pub fn home_config_path() -> String { + env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", home_path())) +} + +pub fn home_cache_path() -> String { + env::var("XDG_CACHE_HOME").unwrap_or(format!("{}/.cache", home_path())) +} + +pub fn home_data_path() -> String { + env::var("XDG_DATA_HOME").unwrap_or(format!("{}/.local/share", home_path())) +} + +/// Expands the environment variables and user home directory in a given path. +pub fn build_path(path: &str) -> Result { + let mut result = String::new(); + let mut chars = path.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '$' { + let mut var_name = String::new(); + while let Some(&c) = chars.peek() { + if !c.is_alphanumeric() && c != '_' { + break; + } + var_name.push(chars.next().unwrap()); + } + if !var_name.is_empty() { + let expanded = if var_name == "HOME" { + home_path() + } else { + env::var(&var_name)? + }; + result.push_str(&expanded); + } else { + result.push('$'); + } + } else if c == '~' && result.is_empty() { + result.push_str(&home_path()) + } else { + result.push(c); + } + } + + Ok(PathBuf::from(result)) +} + +pub fn format_bytes(bytes: u64) -> String { + let kb = 1024u64; + let mb = kb * 1024; + let gb = mb * 1024; + + match bytes { + b if b >= gb => format!("{:.2} GiB", b as f64 / gb as f64), + b if b >= mb => format!("{:.2} MiB", b as f64 / mb as f64), + b if b >= kb => format!("{:.2} KiB", b as f64 / kb as f64), + _ => format!("{} B", bytes), + } +} + +pub fn parse_size(size_str: &str) -> Option { + let size_str = size_str.trim(); + let units = [ + ("B", 1u64), + ("KB", 1000u64), + ("MB", 1000u64 * 1000), + ("GB", 1000u64 * 1000 * 1000), + ("KiB", 1024u64), + ("MiB", 1024u64 * 1024), + ("GiB", 1024u64 * 1024 * 1024), + ]; + + for (unit, multiplier) in &units { + let size_str = size_str.to_uppercase(); + if size_str.ends_with(unit) { + let number_part = size_str.trim_end_matches(unit).trim(); + if let Ok(num) = number_part.parse::() { + return Some((num * (*multiplier as f64)) as u64); + } + } + } + + None +} + +pub fn calculate_checksum(file_path: &Path) -> Result { + let mut hasher = blake3::Hasher::new(); + hasher.update_mmap(file_path)?; + Ok(hasher.finalize().to_hex().to_string()) +} + +pub fn validate_checksum(checksum: &str, file_path: &Path) -> Result<()> { + let final_checksum = calculate_checksum(file_path)?; + if final_checksum == *checksum { + Ok(()) + } else { + Err(SoarError::InvalidChecksum) + } +} + +pub fn setup_required_paths() -> Result<()> { + if !bin_path().exists() { + fs::create_dir_all(bin_path())?; + } + + if !db_path().exists() { + fs::create_dir_all(db_path())? + } + + if !packages_path().exists() { + fs::create_dir_all(packages_path())?; + } + + Ok(()) +} + +pub fn calc_magic_bytes>(file_path: P, size: usize) -> Result> { + let file = File::open(file_path)?; + let mut file = BufReader::new(file); + let mut magic_bytes = vec![0u8; size]; + file.read_exact(&mut magic_bytes)?; + file.rewind().unwrap(); + Ok(magic_bytes) +} + +pub fn create_symlink>(from: P, to: P) -> SoarResult<()> { + let to = to.as_ref(); + if to.is_symlink() { + fs::remove_file(to)?; + } + os::unix::fs::symlink(from, to)?; + Ok(()) +} + +pub fn cleanup() -> Result<()> { + let entries = fs::read_dir(cache_path().join("bin"))?; + + for entry in entries { + let path = entry?.path(); + + let modified_at = path.metadata()?.modified()?; + let elapsed = modified_at.elapsed()?.as_secs(); + let cache_ttl = 28800u64; + + if cache_ttl.saturating_sub(elapsed) == 0 { + fs::remove_file(path)?; + } + } + + remove_broken_symlink() +} + +pub fn remove_broken_symlink() -> Result<()> { + let entries = fs::read_dir(bin_path())?; + for entry in entries { + let path = entry?.path(); + if !path.is_file() { + fs::remove_file(path)?; + } + } + + Ok(()) +} diff --git a/soar-db/migrations/core/V1__initial.sql b/soar-db/migrations/core/V1__initial.sql index 5e3b34d..1668940 100644 --- a/soar-db/migrations/core/V1__initial.sql +++ b/soar-db/migrations/core/V1__initial.sql @@ -66,8 +66,11 @@ CREATE TABLE packages ( build_log TEXT NOT NULL, category TEXT, installed_path TEXT NOT NULL, - installed_date TEXT NOT NULL, + bin_path TEXT, + installed_date TEXT, disabled BOOLEAN NOT NULL DEFAULT false, pinned BOOLEAN NOT NULL DEFAULT false, + is_installed BOOLEAN NOT NULL DEFAULT false, + installed_with_family BOOLEAN NOT NULL DEFAULT false, UNIQUE (family, pkg_name, checksum) ); diff --git a/src/core/color.rs b/src/core/color.rs deleted file mode 100644 index 782f912..0000000 --- a/src/core/color.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::fmt::Display; - -#[derive(Debug, Clone, Copy)] -pub enum Color { - Reset, - Black, - Red, - Green, - Yellow, - Blue, - Magenta, - Cyan, - White, - BrightBlack, - BrightRed, - BrightGreen, - BrightYellow, - BrightBlue, - BrightMagenta, - BrightCyan, - BrightWhite, -} - -impl Color { - pub fn to_ansi_code(self, is_bg: bool) -> &'static str { - match self { - Color::Reset => "\x1B[0m", - Color::Black => { - if is_bg { - "\x1B[40m" - } else { - "\x1B[30m" - } - } - Color::Red => { - if is_bg { - "\x1B[41m" - } else { - "\x1B[31m" - } - } - Color::Green => { - if is_bg { - "\x1B[42m" - } else { - "\x1B[32m" - } - } - Color::Yellow => { - if is_bg { - "\x1B[43m" - } else { - "\x1B[33m" - } - } - Color::Blue => { - if is_bg { - "\x1B[44m" - } else { - "\x1B[34m" - } - } - Color::Magenta => { - if is_bg { - "\x1B[45m" - } else { - "\x1B[35m" - } - } - Color::Cyan => { - if is_bg { - "\x1B[46m" - } else { - "\x1B[36m" - } - } - Color::White => { - if is_bg { - "\x1B[47m" - } else { - "\x1B[37m" - } - } - Color::BrightBlack => { - if is_bg { - "\x1B[100m" - } else { - "\x1B[90m" - } - } - Color::BrightRed => { - if is_bg { - "\x1B[101m" - } else { - "\x1B[91m" - } - } - Color::BrightGreen => { - if is_bg { - "\x1B[102m" - } else { - "\x1B[92m" - } - } - Color::BrightYellow => { - if is_bg { - "\x1B[103m" - } else { - "\x1B[93m" - } - } - Color::BrightBlue => { - if is_bg { - "\x1B[104m" - } else { - "\x1B[94m" - } - } - Color::BrightMagenta => { - if is_bg { - "\x1B[105m" - } else { - "\x1B[95m" - } - } - Color::BrightCyan => { - if is_bg { - "\x1B[106m" - } else { - "\x1B[96m" - } - } - Color::BrightWhite => { - if is_bg { - "\x1B[107m" - } else { - "\x1B[97m" - } - } - } - } -} - -pub trait ColorExt { - fn color(self, color: Color) -> String; - fn bg_color(self, color: Color) -> String; - fn bold(self) -> String; -} - -impl ColorExt for T { - fn color(self, color: Color) -> String { - format!( - "{}{}{}", - color.to_ansi_code(false), - self, - Color::Reset.to_ansi_code(false) - ) - } - - fn bg_color(self, color: Color) -> String { - format!( - "{}{}{}", - color.to_ansi_code(true), - self, - Color::Reset.to_ansi_code(true) - ) - } - - fn bold(self) -> String { - format!( - "{}\x1B[1m{}{}", - Color::Reset.to_ansi_code(false), - self, - Color::Reset.to_ansi_code(false) - ) - } -} diff --git a/src/core/constant.rs b/src/core/constant.rs deleted file mode 100644 index 9191698..0000000 --- a/src/core/constant.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::{path::PathBuf, sync::LazyLock}; - -use super::{config::CONFIG, util::build_path}; - -pub static ROOT_PATH: LazyLock = LazyLock::new(|| build_path(&CONFIG.soar_root).unwrap()); -pub static CACHE_PATH: LazyLock = - LazyLock::new(|| build_path(&CONFIG.soar_cache.clone().unwrap()).unwrap()); -pub static REGISTRY_PATH: LazyLock = LazyLock::new(|| ROOT_PATH.join("registry")); -pub static BIN_PATH: LazyLock = - LazyLock::new(|| build_path(&CONFIG.soar_bin.clone().unwrap()).unwrap()); -pub static INSTALL_TRACK_PATH: LazyLock = LazyLock::new(|| ROOT_PATH.join("installs")); -pub static PACKAGES_PATH: LazyLock = LazyLock::new(|| ROOT_PATH.join("packages")); - -pub const ELF_MAGIC_BYTES: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46]; -pub const APPIMAGE_MAGIC_BYTES: [u8; 4] = [0x41, 0x49, 0x02, 0x00]; -pub const FLATIMAGE_MAGIC_BYTES: [u8; 4] = [0x46, 0x49, 0x01, 0x00]; - -pub const CAP_SYS_ADMIN: i32 = 21; -pub const CAP_MKNOD: i32 = 27; diff --git a/src/core/mod.rs b/src/core/mod.rs deleted file mode 100644 index c228788..0000000 --- a/src/core/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod color; -pub mod config; -pub mod constant; -pub mod file; -pub mod log; -pub mod util; diff --git a/src/core/util.rs b/src/core/util.rs deleted file mode 100644 index dfbc2e9..0000000 --- a/src/core/util.rs +++ /dev/null @@ -1,422 +0,0 @@ -use std::{ - env, - ffi::CStr, - io::Write, - mem, - path::{Path, PathBuf}, -}; - -use anyhow::{Context, Result}; -use futures::StreamExt; -use indicatif::{ProgressState, ProgressStyle}; -use libc::{geteuid, getpwuid, ioctl, winsize, STDOUT_FILENO, TIOCGWINSZ}; -use termion::cursor; -use tokio::{ - fs::{self, File}, - io::{AsyncReadExt, AsyncWriteExt}, -}; -use tracing::info; - -use crate::core::constant::ROOT_PATH; - -use super::{ - color::{Color, ColorExt}, - constant::{BIN_PATH, CACHE_PATH, INSTALL_TRACK_PATH, PACKAGES_PATH, REGISTRY_PATH}, -}; - -fn get_username() -> Result { - unsafe { - let uid = geteuid(); - let pwd = getpwuid(uid); - if pwd.is_null() { - anyhow::bail!("Failed to get user"); - } - let username = CStr::from_ptr((*pwd).pw_name) - .to_string_lossy() - .into_owned(); - Ok(username) - } -} - -pub fn home_path() -> String { - env::var("HOME").unwrap_or_else(|_| { - let username = env::var("USER") - .or_else(|_| env::var("LOGNAME")) - .or_else(|_| get_username().map_err(|_| ())) - .unwrap_or_else(|_| panic!("Couldn't determine username. Please fix the system.")); - format!("/home/{}", username) - }) -} - -pub fn home_config_path() -> String { - env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", home_path())) -} - -pub fn home_cache_path() -> String { - env::var("XDG_CACHE_HOME").unwrap_or(format!("{}/.cache", home_path())) -} - -pub fn home_data_path() -> String { - env::var("XDG_DATA_HOME").unwrap_or(format!("{}/.local/share", home_path())) -} - -/// Expands the environment variables and user home directory in a given path. -pub fn build_path(path: &str) -> Result { - let mut result = String::new(); - let mut chars = path.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '$' { - let mut var_name = String::new(); - while let Some(&c) = chars.peek() { - if !c.is_alphanumeric() && c != '_' { - break; - } - var_name.push(chars.next().unwrap()); - } - if !var_name.is_empty() { - let expanded = if var_name == "HOME" { - home_path() - } else { - env::var(&var_name) - .with_context(|| format!("Environment variable ${} not found", var_name))? - }; - result.push_str(&expanded); - } else { - result.push('$'); - } - } else if c == '~' && result.is_empty() { - result.push_str(&home_path()) - } else { - result.push(c); - } - } - - Ok(PathBuf::from(result)) -} - -pub fn format_bytes(bytes: u64) -> String { - let kb = 1024u64; - let mb = kb * 1024; - let gb = mb * 1024; - - match bytes { - b if b >= gb => format!("{:.2} GiB", b as f64 / gb as f64), - b if b >= mb => format!("{:.2} MiB", b as f64 / mb as f64), - b if b >= kb => format!("{:.2} KiB", b as f64 / kb as f64), - _ => format!("{} B", bytes), - } -} - -pub fn parse_size(size_str: &str) -> Option { - let size_str = size_str.trim(); - let units = [ - ("B", 1u64), - ("KB", 1000u64), - ("MB", 1000u64 * 1000), - ("GB", 1000u64 * 1000 * 1000), - ("KiB", 1024u64), - ("MiB", 1024u64 * 1024), - ("GiB", 1024u64 * 1024 * 1024), - ]; - - for (unit, multiplier) in &units { - let size_str = size_str.to_uppercase(); - if size_str.ends_with(unit) { - let number_part = size_str.trim_end_matches(unit).trim(); - if let Ok(num) = number_part.parse::() { - return Some((num * (*multiplier as f64)) as u64); - } - } - } - - None -} - -pub async fn calculate_checksum(file_path: &Path) -> Result { - let mut file = File::open(&file_path).await?; - - let mut hasher = blake3::Hasher::new(); - let mut buffer = [0u8; 8192]; - - while let Ok(n) = file.read(&mut buffer).await { - if n == 0 { - break; - } - hasher.update(&buffer[..n]); - } - - file.flush().await?; - - Ok(hasher.finalize().to_hex().to_string()) -} - -pub async fn validate_checksum(checksum: &str, file_path: &Path) -> Result<()> { - let final_checksum = calculate_checksum(file_path).await?; - if final_checksum == *checksum { - Ok(()) - } else { - Err(anyhow::anyhow!("Checksum verification failed.")) - } -} - -pub async fn setup_required_paths() -> Result<()> { - if !BIN_PATH.exists() { - fs::create_dir_all(&*BIN_PATH).await.with_context(|| { - format!( - "Failed to create bin directory {}", - BIN_PATH.to_string_lossy().color(Color::Blue) - ) - })?; - } - - if !REGISTRY_PATH.exists() { - fs::create_dir_all(&*REGISTRY_PATH).await.with_context(|| { - format!( - "Failed to create registry directory: {}", - REGISTRY_PATH.display().color(Color::Blue) - ) - })?; - } - - if !INSTALL_TRACK_PATH.exists() { - fs::create_dir_all(&*INSTALL_TRACK_PATH) - .await - .with_context(|| { - format!( - "Failed to create installs directory: {}", - INSTALL_TRACK_PATH.to_string_lossy().color(Color::Blue) - ) - })?; - } - - if !PACKAGES_PATH.exists() { - fs::create_dir_all(&*PACKAGES_PATH).await.with_context(|| { - format!( - "Failed to create packages directory: {}", - PACKAGES_PATH.to_string_lossy().color(Color::Blue) - ) - })?; - } - - Ok(()) -} - -pub async fn download(url: &str, what: &str, silent: bool) -> Result> { - let client = reqwest::Client::new(); - let response = client.get(url).send().await?; - - if !response.status().is_success() { - return Err(anyhow::anyhow!( - "Error fetching {} from {} [{}]", - what.color(Color::Cyan), - url.color(Color::Blue), - response.status().color(Color::Red) - )); - } - - let mut content = Vec::new(); - - if !silent { - info!( - "Fetching {} from {} [{}]", - what.color(Color::Cyan), - url.color(Color::Blue), - format_bytes(response.content_length().unwrap_or_default()) - ); - } - - let mut stream = response.bytes_stream(); - - while let Some(chunk) = stream.next().await { - let chunk = chunk.context("Failed to read chunk")?; - content.extend_from_slice(&chunk); - } - - Ok(content) -} - -pub async fn cleanup() -> Result<()> { - let mut tree = fs::read_dir(&*CACHE_PATH).await?; - - while let Some(entry) = tree.next_entry().await? { - let path = entry.path(); - if xattr::get(&path, "user.managed_by")?.as_deref() != Some(b"soar") { - continue; - }; - - let modified_at = path.metadata()?.modified()?; - let elapsed = modified_at.elapsed()?.as_secs(); - let cache_ttl = 28800u64; - - if cache_ttl.saturating_sub(elapsed) == 0 { - fs::remove_file(path).await?; - } - } - - remove_broken_symlink().await?; - - Ok(()) -} - -pub async fn remove_broken_symlink() -> Result<()> { - let mut tree = fs::read_dir(&*BIN_PATH).await?; - while let Some(entry) = tree.next_entry().await? { - let path = entry.path(); - if !path.is_file() { - fs::remove_file(path).await?; - } - } - - Ok(()) -} - -pub fn wrap_text(text: &str, available_width: usize, indent: u16) -> String { - let mut wrapped_text = String::new(); - let mut current_line_length = 0; - let mut current_ansi_sequence = String::new(); - let mut chars = text.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '\x1B' { - // Start of ANSI escape sequence - current_ansi_sequence.push(c); - while let Some(&next_c) = chars.peek() { - if !next_c.is_ascii_alphabetic() { - current_ansi_sequence.push(chars.next().unwrap()); - } else { - current_ansi_sequence.push(chars.next().unwrap()); - wrapped_text.push_str(¤t_ansi_sequence); - current_ansi_sequence.clear(); - break; - } - } - } else { - // Regular character - if current_line_length >= available_width { - wrapped_text.push('\n'); - wrapped_text.push_str(&cursor::Right(indent).to_string()); - current_line_length = 0; - } - wrapped_text.push(c); - current_line_length += 1; - } - } - - wrapped_text -} - -pub fn get_font_height() -> usize { - let mut w: winsize = unsafe { mem::zeroed() }; - - if unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut w) } == 0 && w.ws_ypixel > 0 && w.ws_row > 0 { - w.ws_ypixel as usize / w.ws_row as usize - } else { - 16 - } -} - -pub fn get_font_width() -> usize { - let mut w: winsize = unsafe { mem::zeroed() }; - - if unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut w) } == 0 && w.ws_xpixel > 0 && w.ws_col > 0 { - w.ws_xpixel as usize / w.ws_col as usize - } else { - 16 - } -} - -pub fn get_terminal_width() -> usize { - let mut w: winsize = unsafe { mem::zeroed() }; - - if unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut w) } == 0 && w.ws_col > 0 { - w.ws_col as usize - } else { - 80 - } -} - -pub fn download_progress_style(with_msg: bool) -> ProgressStyle { - let style = if with_msg { - ProgressStyle::with_template( - "{msg:32!} [{wide_bar:.green/white}] {speed:14} {computed_bytes:22}", - ) - .unwrap() - } else { - ProgressStyle::with_template("[{wide_bar:.green/white}] {speed:14} {computed_bytes:22}") - .unwrap() - }; - - style - .with_key( - "computed_bytes", - |state: &ProgressState, w: &mut dyn std::fmt::Write| { - write!( - w, - "{}/{}", - format_bytes(state.pos()), - format_bytes(state.len().unwrap_or_default()) - ) - .unwrap() - }, - ) - .with_key( - "speed", - |state: &ProgressState, w: &mut dyn std::fmt::Write| { - let pos = state.pos() as f64; - let elapsed = state.elapsed().as_secs_f64(); - let speed = if elapsed > 0.0 { - (pos / elapsed) as u64 - } else { - 0 - }; - write!(w, "{}/s", format_bytes(speed)).unwrap() - }, - ) - .progress_chars("━━") -} - -#[derive(PartialEq, Eq)] -pub enum AskType { - Warn, - Normal, -} - -pub fn interactive_ask(ques: &str, ask_type: AskType) -> Result { - print!( - "{}{ques}", - if ask_type == AskType::Warn { - "[WARN]".color(Color::BrightYellow) - } else { - "".to_owned() - } - ); - - std::io::stdout().flush()?; - - let mut response = String::new(); - std::io::stdin().read_line(&mut response)?; - - Ok(response.trim().to_owned()) -} - -pub fn print_env() { - let root_path = ROOT_PATH - .is_symlink() - .then(|| ROOT_PATH.read_link().unwrap()) - .unwrap_or(ROOT_PATH.to_path_buf()); - - let bin_path = BIN_PATH - .is_symlink() - .then(|| BIN_PATH.read_link().unwrap()) - .unwrap_or(BIN_PATH.to_path_buf()); - - let cache_path = CACHE_PATH - .is_symlink() - .then(|| CACHE_PATH.read_link().unwrap()) - .unwrap_or(CACHE_PATH.to_path_buf()); - - info!("SOAR_ROOT={}", root_path.display()); - info!("SOAR_BIN={}", bin_path.display()); - info!("SOAR_CACHE={}", cache_path.display()); -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index bb0779e..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,242 +0,0 @@ -use anyhow::Result; -use clap::Parser; -use cli::{Args, Commands, SelfAction}; -use misc::{ - download::{download, download_and_save, github::fetch_github_releases, ApiType}, - health::check_health, -}; -use package::build; -use registry::PackageRegistry; -use tokio::fs; -use tracing::{debug, error, info, trace, warn}; - -use core::{ - color::{Color, ColorExt}, - config::{self, generate_default_config}, - constant::BIN_PATH, - log::setup_logging, - util::{cleanup, print_env, setup_required_paths}, -}; -use std::{ - env::{self, consts::ARCH}, - io::Read, - path::Path, -}; - -mod cli; -pub mod core; -mod misc; -mod package; -mod registry; - -async fn handle_cli() -> Result<()> { - let mut args = env::args().collect::>(); - let self_bin = args.get(0).unwrap().clone(); - let self_version = env!("CARGO_PKG_VERSION"); - - let mut i = 0; - while i < args.len() { - if args[i] == "-" { - let mut stdin = std::io::stdin(); - let mut buffer = String::new(); - if stdin.read_to_string(&mut buffer).is_ok() { - let stdin_args = buffer.split_whitespace().collect::>(); - args.remove(i); - args.splice(i..i, stdin_args.into_iter().map(String::from)); - } else { - i += 1; - } - } else { - i += 1; - } - } - - let args = Args::parse_from(args); - - setup_logging(&args); - - debug!("Initializing soar"); - config::init(); - - debug!("Setting up paths"); - setup_required_paths().await?; - - let path_env = env::var("PATH")?; - if !path_env.split(':').any(|p| Path::new(p) == *BIN_PATH) { - warn!( - "{} is not in {1}. Please add it to {1} to use installed binaries.", - &*BIN_PATH.to_string_lossy().color(Color::Blue), - "PATH".color(Color::BrightGreen).bold() - ); - } - - debug!("Initializing package registry"); - let registry = PackageRegistry::new(); - - trace!("Running cleanup"); - let _ = cleanup().await; - - match args.command { - Commands::Install { - packages, - force, - portable, - portable_home, - portable_config, - yes, - } => { - if portable.is_some() && (portable_home.is_some() || portable_config.is_some()) { - error!("--portable cannot be used with --portable-home or --portable-config"); - std::process::exit(1); - } - - let portable = portable.map(|p| p.unwrap_or_default()); - let portable_home = portable_home.map(|p| p.unwrap_or_default()); - let portable_config = portable_config.map(|p| p.unwrap_or_default()); - - registry - .await? - .install_packages( - &packages, - force, - portable, - portable_home, - portable_config, - yes, - args.quiet, - ) - .await?; - } - Commands::Sync => { - registry.await?; - } - Commands::Remove { packages, exact } => { - registry.await?.remove_packages(&packages, exact).await?; - } - Commands::Update { packages } => { - registry - .await? - .update(packages.as_deref(), args.quiet) - .await?; - } - Commands::ListInstalledPackages { packages } => { - registry.await?.info(packages.as_deref()).await?; - } - Commands::Search { - query, - case_sensitive, - limit, - } => { - registry - .await? - .search(&query, case_sensitive, limit) - .await?; - } - Commands::Query { query } => { - registry.await?.query(&query).await?; - } - Commands::ListPackages { collection } => { - registry.await?.list(collection.as_deref()).await?; - } - Commands::Inspect { package } => { - registry.await?.inspect(&package, "script").await?; - } - Commands::Log { package } => { - registry.await?.inspect(&package, "log").await?; - } - Commands::Run { command, yes } => { - registry.await?.run(command.as_ref(), yes).await?; - } - Commands::Use { package } => { - registry.await?.use_package(&package, args.quiet).await?; - } - Commands::Download { - links, - yes, - output, - regex_patterns, - match_keywords, - exclude_keywords, - } => { - download_and_save( - registry.await?, - links.as_ref(), - yes, - output, - regex_patterns.as_deref(), - match_keywords.as_deref(), - exclude_keywords.as_deref(), - ) - .await?; - } - Commands::Health => { - check_health().await; - } - Commands::DefConfig => { - generate_default_config()?; - } - Commands::Env => { - print_env(); - } - Commands::Build { files } => { - for file in files { - build::init(&file).await?; - } - } - Commands::SelfCmd { action } => { - match action { - SelfAction::Update => { - let is_nightly = self_version.starts_with("nightly"); - let gh_releases = - fetch_github_releases(&ApiType::PkgForge, "pkgforge/soar").await?; - - let release = gh_releases.iter().find(|rel| { - if is_nightly { - return rel.name.starts_with("nightly") && rel.name != self_version; - } else { - rel.tag_name - .trim_start_matches('v') - .parse::() - .map(|v| v > self_version.parse::().unwrap()) - .unwrap_or(false) - } - }); - if let Some(release) = release { - let asset = release - .assets - .iter() - .find(|a| { - a.name.contains(ARCH) - && !a.name.contains("tar") - && !a.name.contains("sum") - }) - .unwrap(); - download(&asset.browser_download_url, Some(self_bin)).await?; - println!("Soar updated to {}", release.tag_name); - } else { - eprintln!("No updates found."); - } - } - SelfAction::Uninstall => { - match fs::remove_file(self_bin).await { - Ok(_) => { - info!("Soar has been uninstalled successfully."); - info!("You should remove soar config and data files manually."); - } - Err(err) => { - error!("{}\nFailed to uninstall soar.", err.to_string()); - } - }; - } - }; - } - }; - - Ok(()) -} - -pub async fn init() { - if let Err(e) = handle_cli().await { - error!("{}", e); - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 6bef831..0000000 --- a/src/main.rs +++ /dev/null @@ -1,10 +0,0 @@ -use soar_cli::init; - -#[tokio::main] -async fn main() { - unsafe { - libc::signal(libc::SIGPIPE, libc::SIG_DFL); - } - - init().await; -} diff --git a/src/misc/download/github.rs b/src/misc/download/github.rs deleted file mode 100644 index 46ddcb2..0000000 --- a/src/misc/download/github.rs +++ /dev/null @@ -1,233 +0,0 @@ -use std::env; - -use anyhow::{Context, Result}; -use regex::Regex; -use reqwest::{ - header::{HeaderMap, AUTHORIZATION, USER_AGENT}, - Response, -}; -use serde::{Deserialize, Serialize}; -use tracing::{debug, error, info, trace}; - -use crate::{ - core::{ - color::{Color, ColorExt}, - util::{format_bytes, interactive_ask, AskType}, - }, - misc::download::download, -}; - -use super::{should_fallback, ApiType}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct GithubAsset { - pub name: String, - pub size: u64, - pub browser_download_url: String, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct GithubRelease { - pub name: String, - pub tag_name: String, - pub draft: bool, - pub prerelease: bool, - pub published_at: String, - pub assets: Vec, -} - -pub static GITHUB_URL_REGEX: &str = - r"^(?i)(?:https?://)?(?:github(?:\.com)?[:/])([^/@]+/[^/@]+)(?:@([^/\s]*)?)?$"; - -async fn call_github_api(gh_api: &ApiType, user_repo: &str) -> Result { - let client = reqwest::Client::new(); - let url = format!( - "{}/repos/{}/releases?per_page=100", - match gh_api { - ApiType::PkgForge => "https://api.gh.pkgforge.dev", - ApiType::Primary => "https://api.github.com", - }, - user_repo - ); - let mut headers = HeaderMap::new(); - headers.insert(USER_AGENT, "pkgforge/soar".parse()?); - if matches!(gh_api, ApiType::Primary) { - if let Ok(token) = env::var("GITHUB_TOKEN") { - trace!("Using Github token: {}", token); - headers.insert(AUTHORIZATION, format!("Bearer {}", token).parse()?); - } - } - client - .get(&url) - .headers(headers) - .send() - .await - .context("Failed to fetch GitHub releases") -} - -pub async fn fetch_github_releases( - gh_api: &ApiType, - user_repo: &str, -) -> Result> { - let response = match call_github_api(gh_api, user_repo).await { - Ok(resp) => { - let status = resp.status(); - if should_fallback(status) && matches!(gh_api, ApiType::PkgForge) { - debug!("Failed to fetch Github asset using pkgforge API. Retrying request using Github API."); - call_github_api(&ApiType::Primary, user_repo).await? - } else { - resp - } - } - Err(e) => return Err(e), - }; - - if !response.status().is_success() { - anyhow::bail!( - "Error fetching releases for {}: {}", - user_repo, - response.status() - ); - } - - let releases: Vec = response - .json() - .await - .context("Failed to parse GitHub response")?; - - Ok(releases) -} - -fn select_asset_idx(assets: &[&GithubAsset], max: usize) -> Result { - for (i, asset) in assets.iter().enumerate() { - info!( - " [{}] {:#?} ({})", - i + 1, - asset.name, - format_bytes(asset.size), - ); - } - let selection = loop { - let response = interactive_ask( - &format!("Select an asset (1-{}): ", assets.len()), - AskType::Normal, - )?; - - match response.parse::() { - Ok(n) if n > 0 && n <= max => break n - 1, - _ => error!("Invalid selection, please try again."), - } - }; - Ok(selection) -} - -pub async fn handle_github_download( - re: &Regex, - link: &str, - output: Option, - match_keywords: Option<&[String]>, - exclude_keywords: Option<&[String]>, - asset_regexes: &Vec, - yes: bool, -) -> Result<()> { - if let Some(caps) = re.captures(link) { - let user_repo = caps.get(1).unwrap().as_str(); - let tag = caps - .get(2) - .map(|tag| tag.as_str().trim()) - .filter(|&tag| !tag.is_empty()); - info!("Fetching releases for {}...", user_repo); - - let releases = fetch_github_releases(&ApiType::PkgForge, user_repo).await?; - - let release = if let Some(tag_name) = tag { - releases - .iter() - .find(|release| release.tag_name.starts_with(tag_name)) - } else { - releases - .iter() - .find(|release| !release.prerelease && !release.draft) - }; - - let Some(release) = release else { - error!( - "No {} found for repository {}", - tag.map(|t| format!("tag {}", t)) - .unwrap_or("stable release".to_owned()), - user_repo - ); - return Ok(()); - }; - - let assets = &release.assets; - - if assets.is_empty() { - error!("No assets found for the release."); - return Ok(()); - } - - let selected_asset = { - let assets: Vec<&GithubAsset> = assets - .iter() - .filter(|asset| { - asset_regexes - .iter() - .all(|regex| regex.is_match(&asset.name)) - && match_keywords.map_or(true, |keywords| { - keywords.iter().all(|keyword| { - keyword - .split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .all(|part| { - asset.name.to_lowercase().contains(&part.to_lowercase()) - }) - }) - }) - && exclude_keywords.map_or(true, |keywords| { - keywords.iter().all(|keyword| { - keyword - .split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .all(|part| { - !asset.name.to_lowercase().contains(&part.to_lowercase()) - }) - }) - }) - }) - .collect(); - - match assets.len() { - 0 => { - error!("No assets matched the provided criteria."); - return Ok(()); - } - 1 => assets[0], - _ => { - if yes { - assets[0] - } else { - info!( - "Multiple matching assets found for {}{}", - release.tag_name, - if release.prerelease { - " [prerelease]".color(Color::BrightRed) - } else { - " [stable]".color(Color::BrightCyan) - } - ); - - let asset_idx = select_asset_idx(&assets, assets.len())?; - assets[asset_idx] - } - } - } - }; - - let download_url = &selected_asset.browser_download_url; - download(download_url, output.clone()).await?; - } - Ok(()) -} diff --git a/src/misc/download/gitlab.rs b/src/misc/download/gitlab.rs deleted file mode 100644 index 88daffa..0000000 --- a/src/misc/download/gitlab.rs +++ /dev/null @@ -1,225 +0,0 @@ -use std::env; - -use anyhow::{Context, Result}; -use regex::Regex; -use reqwest::{ - header::{HeaderMap, AUTHORIZATION, USER_AGENT}, - Response, -}; -use serde::{Deserialize, Serialize}; -use tracing::{debug, error, info, trace}; - -use crate::{ - core::{ - color::{Color, ColorExt}, - util::{interactive_ask, AskType}, - }, - misc::download::download, -}; - -use super::{should_fallback, ApiType}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct GitlabAsset { - name: String, - direct_asset_url: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct GitlabAssets { - links: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -struct GitlabRelease { - tag_name: String, - upcoming_release: bool, - released_at: String, - assets: GitlabAssets, -} - -pub static GITLAB_URL_REGEX: &str = - r"^(?i)(?:https?://)?(?:gitlab(?:\.com)?[:/])([^/@]+/[^/@]+)(?:@([^/\s]*)?)?$"; - -async fn call_gitlab_api(gh_api: &ApiType, user_repo: &str) -> Result { - let client = reqwest::Client::new(); - let url = format!( - "{}/api/v4/projects/{}/releases", - match gh_api { - ApiType::PkgForge => "https://api.gl.pkgforge.dev", - ApiType::Primary => "https://gitlab.com", - }, - user_repo.replace("/", "%2F") - ); - let mut headers = HeaderMap::new(); - headers.insert(USER_AGENT, "pkgforge/soar".parse()?); - if matches!(gh_api, ApiType::Primary) { - if let Ok(token) = env::var("GITLAB_TOKEN") { - trace!("Using Gitlab token: {}", token); - headers.insert(AUTHORIZATION, format!("Bearer {}", token).parse()?); - } - } - client - .get(&url) - .headers(headers) - .send() - .await - .context("Failed to fetch Gitlab releases") -} - -async fn fetch_gitlab_releases(gh_api: &ApiType, user_repo: &str) -> Result> { - let response = match call_gitlab_api(gh_api, user_repo).await { - Ok(resp) => { - let status = resp.status(); - if should_fallback(status) && matches!(gh_api, ApiType::PkgForge) { - debug!("Failed to fetch Gitlab asset using pkgforge API. Retrying request using Gitlab API."); - call_gitlab_api(&ApiType::Primary, user_repo).await? - } else { - resp - } - } - Err(e) => return Err(e), - }; - - if !response.status().is_success() { - anyhow::bail!( - "Error fetching releases for {}: {}", - user_repo, - response.status() - ); - } - - let releases: Vec = response - .json() - .await - .context("Failed to parse Gitlab response")?; - - Ok(releases) -} - -fn select_asset_idx(assets: &[&GitlabAsset], max: usize) -> Result { - for (i, asset) in assets.iter().enumerate() { - info!(" [{}] {}", i + 1, asset.name); - } - let selection = loop { - let response = interactive_ask( - &format!("Select an asset (1-{}): ", assets.len()), - AskType::Normal, - )?; - - match response.parse::() { - Ok(n) if n > 0 && n <= max => break n - 1, - _ => error!("Invalid selection, please try again."), - } - }; - Ok(selection) -} - -pub async fn handle_gitlab_download( - re: &Regex, - link: &str, - output: Option, - match_keywords: Option<&[String]>, - exclude_keywords: Option<&[String]>, - asset_regexes: &Vec, - yes: bool, -) -> Result<()> { - if let Some(caps) = re.captures(link) { - let user_repo = caps.get(1).unwrap().as_str(); - let tag = caps - .get(2) - .map(|tag| tag.as_str().trim()) - .filter(|&tag| !tag.is_empty()); - info!("Fetching releases for {}...", user_repo); - - let releases = fetch_gitlab_releases(&ApiType::PkgForge, user_repo).await?; - - let release = if let Some(tag_name) = tag { - releases - .iter() - .find(|release| release.tag_name.starts_with(tag_name)) - } else { - releases.iter().find(|release| !release.upcoming_release) - }; - - let Some(release) = release else { - error!( - "No {} found for repository {}", - tag.map(|t| format!("tag {}", t)) - .unwrap_or("stable release".to_owned()), - user_repo - ); - return Ok(()); - }; - - let assets = &release.assets.links; - - if assets.is_empty() { - error!("No assets found for the release."); - return Ok(()); - } - - let selected_asset = { - let assets: Vec<&GitlabAsset> = assets - .iter() - .filter(|asset| { - asset_regexes - .iter() - .all(|regex| regex.is_match(&asset.name)) - && match_keywords.map_or(true, |keywords| { - keywords.iter().all(|keyword| { - keyword - .split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .all(|part| { - asset.name.to_lowercase().contains(&part.to_lowercase()) - }) - }) - }) - && exclude_keywords.map_or(true, |keywords| { - keywords.iter().all(|keyword| { - keyword - .split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .all(|part| { - !asset.name.to_lowercase().contains(&part.to_lowercase()) - }) - }) - }) - }) - .collect(); - - match assets.len() { - 0 => { - error!("No assets matched the provided criteria."); - return Ok(()); - } - 1 => assets[0], - _ => { - if yes { - assets[0] - } else { - info!( - "Multiple matching assets found for {}{}", - release.tag_name, - if release.upcoming_release { - " [prerelease]".color(Color::BrightRed) - } else { - " [stable]".color(Color::BrightCyan) - } - ); - - let asset_idx = select_asset_idx(&assets, assets.len())?; - assets[asset_idx] - } - } - } - }; - - let download_url = &selected_asset.direct_asset_url; - download(download_url, output.clone()).await?; - } - Ok(()) -} diff --git a/src/misc/download/mod.rs b/src/misc/download/mod.rs deleted file mode 100644 index 46499d9..0000000 --- a/src/misc/download/mod.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::Path}; - -use anyhow::{Context, Result}; -use chrono::Utc; -use futures::StreamExt; -use github::{handle_github_download, GITHUB_URL_REGEX}; -use gitlab::{handle_gitlab_download, GITLAB_URL_REGEX}; -use indicatif::ProgressBar; -use regex::Regex; -use reqwest::{header::USER_AGENT, StatusCode, Url}; -use tokio::{ - fs::{self, File}, - io::{AsyncReadExt, AsyncWriteExt, BufReader}, -}; -use tracing::{error, info}; - -pub mod github; -mod gitlab; - -use crate::{ - core::{ - color::{Color, ColorExt}, - constant::ELF_MAGIC_BYTES, - util::{download_progress_style, format_bytes}, - }, - package::parse_package_query, - registry::{select_single_package, PackageRegistry}, -}; - -pub enum ApiType { - PkgForge, - Primary, -} - -fn extract_filename(url: &str) -> String { - Path::new(url) - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_else(|| { - let dt = Utc::now().timestamp(); - dt.to_string() - }) -} - -async fn is_elf(file_path: &Path) -> bool { - let Ok(file) = File::open(file_path).await else { - return false; - }; - let mut file = BufReader::new(file); - - let mut magic_bytes = [0_u8; 4]; - if file.read_exact(&mut magic_bytes).await.is_ok() { - return magic_bytes == ELF_MAGIC_BYTES; - } - false -} - -pub async fn download(url: &str, output: Option) -> Result<()> { - let client = reqwest::Client::new(); - let response = client - .get(url) - .header(USER_AGENT, "pkgforge/soar") - .send() - .await?; - - if !response.status().is_success() { - return Err(anyhow::anyhow!( - "Error fetching {} [{}]", - url.color(Color::Blue), - response.status().color(Color::Red) - )); - } - - let filename = output.unwrap_or(extract_filename(url)); - let filename = if filename.ends_with("/") { - format!( - "{}/{}", - filename.trim_end_matches("/"), - extract_filename(url) - ) - } else { - filename - }; - let output_path = Path::new(&filename); - - if let Some(output_dir) = output_path.parent() { - if !output_dir.exists() { - fs::create_dir_all(&output_dir).await.context(format!( - "Failed to create directory: {}", - output_dir.display() - ))?; - } - } - - let temp_path = format!("{}.tmp", output_path.display()); - - info!( - "Downloading file from {} [{}]", - url.color(Color::Blue), - format_bytes(response.content_length().unwrap_or_default()).color(Color::Yellow) - ); - - let content_length = response.content_length().unwrap_or(0); - let progress_bar = ProgressBar::new(content_length); - progress_bar.set_style(download_progress_style(false)); - - let mut stream = response.bytes_stream(); - let mut file = fs::OpenOptions::new() - .write(true) - .create(true) - .append(true) - .open(&temp_path) - .await - .context("Failed to open temp file for writing")?; - - let mut downloaded_bytes = 0u64; - while let Some(chunk) = stream.next().await { - let chunk = chunk.context("Failed to read chunk")?; - file.write_all(&chunk).await?; - downloaded_bytes = downloaded_bytes.saturating_add(chunk.len() as u64); - progress_bar.set_position(downloaded_bytes); - if content_length == 0 { - progress_bar.set_length(downloaded_bytes); - } - } - progress_bar.finish(); - - fs::rename(&temp_path, &output_path).await?; - - if is_elf(output_path).await { - fs::set_permissions(&output_path, Permissions::from_mode(0o755)).await?; - } - - info!("Downloaded {}", output_path.display().color(Color::Blue)); - - Ok(()) -} - -fn should_fallback(status: StatusCode) -> bool { - status == StatusCode::TOO_MANY_REQUESTS - || status == StatusCode::UNAUTHORIZED - || status == StatusCode::FORBIDDEN - || status.is_server_error() -} - -pub async fn download_and_save( - registry: PackageRegistry, - links: &[String], - yes: bool, - output: Option, - regex_patterns: Option<&[String]>, - match_keywords: Option<&[String]>, - exclude_keywords: Option<&[String]>, -) -> Result<()> { - let github_re = Regex::new(GITHUB_URL_REGEX).unwrap(); - let gitlab_re = Regex::new(GITLAB_URL_REGEX).unwrap(); - let asset_regexes = regex_patterns - .map(|patterns| { - patterns - .iter() - .map(|pattern| Regex::new(pattern)) - .collect::, regex::Error>>() - }) - .transpose()? - .unwrap_or_default(); - - for link in links { - let link = link.trim(); - if github_re.is_match(link) { - info!( - "GitHub repository URL detected: {}", - link.color(Color::Blue) - ); - handle_github_download( - &github_re, - link, - output.clone(), - match_keywords, - exclude_keywords, - &asset_regexes, - yes, - ) - .await?; - } else if gitlab_re.is_match(link) { - info!( - "Gitlab repository URL detected: {}", - link.color(Color::Blue) - ); - handle_gitlab_download( - &gitlab_re, - link, - output.clone(), - match_keywords, - exclude_keywords, - &asset_regexes, - yes, - ) - .await?; - } else if let Ok(url) = Url::parse(link) { - download(url.as_str(), output.clone()).await?; - } else { - error!("{} is not a valid URL", link.color(Color::Blue)); - info!("Searching for package instead.."); - - let query = parse_package_query(link); - let packages = registry.storage.get_packages(&query); - - if let Some(packages) = packages { - let resolved_pkg = if yes || packages.len() == 1 { - &packages[0] - } else { - select_single_package(&packages)? - }; - download(&resolved_pkg.package.download_url, output.clone()).await?; - } else { - error!("No packages found."); - } - }; - } - - Ok(()) -} diff --git a/src/misc/health.rs b/src/misc/health.rs deleted file mode 100644 index c7292c5..0000000 --- a/src/misc/health.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::{cmp::Ordering, future::Future, os::unix::fs::PermissionsExt, path::Path, pin::Pin}; - -use futures::future::join_all; -use libc::{fork, unshare, waitpid, CLONE_NEWUSER, PR_CAPBSET_READ}; -use tokio::fs; -use tracing::{info, warn}; - -use crate::core::{ - color::{Color, ColorExt}, - constant::{CAP_MKNOD, CAP_SYS_ADMIN}, -}; - -fn check_capability(cap: i32) -> bool { - unsafe { libc::prctl(PR_CAPBSET_READ, cap, 0, 0) == 1 } -} - -pub async fn check_health() { - let mut errors = Vec::new(); - - let pid = unsafe { fork() }; - match pid.cmp(&0) { - Ordering::Equal => { - if unsafe { unshare(CLONE_NEWUSER) != 0 } { - errors.push("You lack permissions to create user_namespaces"); - } - std::process::exit(0); - } - Ordering::Greater => { - unsafe { - waitpid(pid, std::ptr::null_mut(), 0); - }; - } - _ => {} - } - - if !Path::new("/proc/self/ns/user").exists() { - errors.push("Your kernel does not support user namespaces"); - } - - let checks: Vec>>>> = vec![ - Box::pin(check_unprivileged_userns_clone()), - Box::pin(check_max_user_namespaces()), - Box::pin(check_userns_restrict()), - Box::pin(check_apparmor_restrict()), - Box::pin(check_capabilities()), - ]; - - info!("{0} FUSE CHECK {0}", "☵".repeat(4)); - check_fusermount().await; - - let results = join_all(checks).await; - - results.into_iter().for_each(|result| { - if let Some(error) = result { - errors.push(error); - } - }); - - info!("\n{0} USER NAMESPACE CHECK {0}", "☵".repeat(4)); - for error in &errors { - warn!("{}", error); - } - - if errors.is_empty() { - info!("User namespace checked successfully.") - } else { - info!( - "{} {}", - "More info at:".color(Color::Cyan), - "https://l.ajam.dev/namespace".color(Color::Blue) - ) - } -} - -async fn check_unprivileged_userns_clone() -> Option<&'static str> { - let content = fs::read_to_string("/proc/sys/kernel/unprivileged_userns_clone") - .await - .ok()?; - if content.trim() == "0" { - Some("You must enable unprivileged_userns_clone") - } else { - None - } -} - -async fn check_max_user_namespaces() -> Option<&'static str> { - let content = fs::read_to_string("/proc/sys/user/max_user_namespaces") - .await - .ok()?; - if content.trim() == "0" { - Some("You must enable max_user_namespaces") - } else { - None - } -} - -async fn check_userns_restrict() -> Option<&'static str> { - let content = fs::read_to_string("/proc/sys/kernel/userns_restrict") - .await - .ok()?; - if content.trim() == "1" { - Some("You must disable userns_restrict") - } else { - None - } -} - -async fn check_apparmor_restrict() -> Option<&'static str> { - let content = fs::read_to_string("/proc/sys/kernel/apparmor_restrict_unprivileged_userns") - .await - .ok()?; - if content.trim() == "1" { - Some("You must disable apparmor_restrict_unprivileged_userns") - } else { - None - } -} - -async fn check_capabilities() -> Option<&'static str> { - if !check_capability(CAP_SYS_ADMIN) { - if !check_capability(CAP_MKNOD) { - return Some("Capability 'CAP_MKNOD' is not available."); - } - return Some("Capability 'CAP_SYS_ADMIN' is not available."); - } - None -} - -async fn check_fusermount() { - let mut error = String::new(); - - let fusermount_path = match which::which("fusermount3") { - Ok(path) => Some(path), - Err(_) => match which::which("fusermount") { - Ok(path) => Some(path), - Err(_) => { - error = format!( - "{} not found. Please install {}.\n", - "fusermount".color(Color::Blue), - "fuse".color(Color::Blue) - ); - None - } - }, - }; - - if let Some(fusermount_path) = fusermount_path { - match fusermount_path.metadata() { - Ok(meta) => { - let permissions = meta.permissions().mode(); - if permissions != 0o104755 { - error = format!( - "Invalid file mode bits. Set 4755 for {}.", - fusermount_path.to_string_lossy().color(Color::Green) - ); - } - } - Err(_) => { - error = "Unable to read fusermount metadata.".to_owned(); - } - } - } - - if !error.is_empty() { - warn!( - "{}\n{} {}", - error, - "More info at:".color(Color::Cyan), - "https://l.ajam.dev/fuse".color(Color::Blue) - ); - } else { - info!("Fuse checked successfully."); - } -} diff --git a/src/misc/mod.rs b/src/misc/mod.rs deleted file mode 100644 index b7fc20b..0000000 --- a/src/misc/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod download; -pub mod health; diff --git a/src/package/appimage.rs b/src/package/appimage.rs deleted file mode 100644 index d875974..0000000 --- a/src/package/appimage.rs +++ /dev/null @@ -1,401 +0,0 @@ -use std::{ - collections::HashSet, - fs::File, - io::{BufReader, BufWriter, Read, Seek, Write}, - path::{Path, PathBuf}, -}; - -use anyhow::{Context, Result}; -use backhand::{kind::Kind, FilesystemReader, InnerNode, Node, SquashfsFileReader}; -use image::{imageops::FilterType, DynamicImage, GenericImageView}; -use tokio::{fs, try_join}; -use tracing::{error, info}; - -use crate::core::{ - color::{Color, ColorExt}, - constant::{BIN_PATH, PACKAGES_PATH}, - util::{download, home_data_path}, -}; - -use super::Package; - -const SUPPORTED_DIMENSIONS: &[(u32, u32)] = &[ - (16, 16), - (24, 24), - (32, 32), - (48, 48), - (64, 64), - (72, 72), - (80, 80), - (96, 96), - (128, 128), - (192, 192), - (256, 256), - (512, 512), -]; - -fn find_offset(file: &mut BufReader) -> Result { - let mut magic = [0_u8; 4]; - // Little-Endian v4.0 - let kind = Kind::from_target("le_v4_0").unwrap(); - while file.read_exact(&mut magic).is_ok() { - if magic == kind.magic() { - let found = file.stream_position()? - magic.len() as u64; - file.rewind()?; - return Ok(found); - } - } - file.rewind()?; - Ok(0) -} - -fn find_nearest_supported_dimension(width: u32, height: u32) -> (u32, u32) { - SUPPORTED_DIMENSIONS - .iter() - .min_by_key(|&&(w, h)| { - let width_diff = (w as i32 - width as i32).abs(); - let height_diff = (h as i32 - height as i32).abs(); - width_diff + height_diff - }) - .cloned() - .unwrap_or((width, height)) -} - -fn normalize_image(image: DynamicImage) -> DynamicImage { - let (width, height) = image.dimensions(); - let (new_width, new_height) = find_nearest_supported_dimension(width, height); - - if (width, height) != (new_width, new_height) { - info!( - "Resizing image from {}x{} to {}x{}", - width, height, new_width, new_height - ); - image.resize(new_width, new_height, FilterType::Lanczos3) - } else { - image - } -} - -async fn create_symlink(from: &Path, to: &Path) -> Result<()> { - if to.is_symlink() { - if to.exists() && !to.read_link()?.starts_with(&*PACKAGES_PATH) { - error!( - "{} is not managed by soar", - to.to_string_lossy().color(Color::Blue) - ); - return Ok(()); - } - fs::remove_file(to).await?; - } - fs::symlink(from, to).await?; - - Ok(()) -} - -async fn remove_link(path: &Path) -> Result<()> { - if path.is_symlink() { - if path.exists() && !path.read_link()?.starts_with(&*PACKAGES_PATH) { - error!( - "{} is not managed by soar", - path.to_string_lossy().color(Color::Blue) - ); - return Ok(()); - } - fs::remove_file(path).await?; - } - Ok(()) -} - -pub async fn remove_applinks(name: &str, bin_name: &str, file_path: &Path) -> Result<()> { - let home_data = home_data_path(); - let data_path = Path::new(&home_data); - - let desktop_path = data_path - .join("applications") - .join(format!("{name}-soar.desktop")); - - remove_link(&desktop_path).await?; - - let original_icon_path = file_path.with_extension("png"); - if original_icon_path.exists() { - let (w, h) = image::image_dimensions(&original_icon_path)?; - - let icon_path = data_path - .join("icons") - .join("hicolor") - .join(format!("{}x{}", w, h)) - .join("apps") - .join(bin_name) - .with_extension("png"); - - remove_link(&icon_path).await?; - } - - Ok(()) -} - -pub async fn integrate_appimage( - file: &mut BufReader, - package: &Package, - file_path: &Path, -) -> Result { - let offset = find_offset(file)?; - let squashfs = FilesystemReader::from_reader_with_offset(file, offset)?; - - let home_data = home_data_path(); - let data_path = Path::new(&home_data); - - for node in squashfs.files() { - let node_path = node.fullpath.to_string_lossy(); - if !node_path.trim_start_matches("/").contains("/") - && (node_path.ends_with(".DirIcon") || node_path.ends_with(".desktop")) - { - let extension = if node_path.ends_with(".DirIcon") { - "png" - } else { - "desktop" - }; - let output_path = file_path.with_extension(extension); - match resolve_and_extract(&squashfs, node, &output_path, &mut HashSet::new()) { - Ok(()) => { - if extension == "png" { - process_icon(&output_path, &package.pkg_name, data_path).await?; - } else { - process_desktop(&output_path, &package.pkg_name, &package.pkg, data_path) - .await?; - } - } - Err(e) => error!("Failed to extract {}: {}", node_path.color(Color::Blue), e), - } - } - } - - Ok(true) -} - -fn resolve_and_extract( - squashfs: &FilesystemReader, - node: &Node, - output_path: &Path, - visited: &mut HashSet, -) -> Result<()> { - match &node.inner { - InnerNode::File(file) => extract_file(squashfs, file, output_path), - InnerNode::Symlink(sym) => { - let target_path = sym.link.clone(); - if !visited.insert(target_path.clone()) { - return Err(anyhow::anyhow!( - "Uh oh. Bad symlink.. Infinite recursion detected..." - )); - } - if let Some(target_node) = squashfs - .files() - .find(|n| n.fullpath.strip_prefix("/").unwrap() == target_path) - { - resolve_and_extract(squashfs, target_node, output_path, visited) - } else { - Err(anyhow::anyhow!("Symlink target not found")) - } - } - _ => Err(anyhow::anyhow!("Unexpected node type")), - } -} - -fn extract_file( - squashfs: &FilesystemReader, - file: &SquashfsFileReader, - output_path: &Path, -) -> Result<()> { - let mut reader = squashfs.file(&file.basic).reader().bytes(); - let output_file = File::create(output_path)?; - let mut buf_writer = BufWriter::new(output_file); - while let Some(Ok(byte)) = reader.next() { - buf_writer.write_all(&[byte])?; - } - Ok(()) -} - -async fn process_icon(output_path: &Path, name: &str, data_path: &Path) -> Result<()> { - let image = image::open(output_path)?; - let (orig_w, orig_h) = image.dimensions(); - - let normalized_image = normalize_image(image); - let (w, h) = normalized_image.dimensions(); - - if (w, h) != (orig_w, orig_h) { - normalized_image.save(output_path)?; - } - let final_path = data_path - .join("icons") - .join("hicolor") - .join(format!("{}x{}", w, h)) - .join("apps") - .join(name) - .with_extension("png"); - - if let Some(parent) = final_path.parent() { - fs::create_dir_all(parent).await.context(anyhow::anyhow!( - "Failed to create icon directory at {}", - parent.to_string_lossy().color(Color::Blue) - ))?; - } - create_symlink(output_path, &final_path).await?; - Ok(()) -} - -async fn process_desktop( - output_path: &Path, - bin_name: &str, - name: &str, - data_path: &Path, -) -> Result<()> { - let mut content = String::new(); - File::open(output_path)?.read_to_string(&mut content)?; - - let processed_content = content - .lines() - .filter(|line| !line.starts_with('#')) - .map(|line| { - if line.starts_with("Icon=") { - format!("Icon={}", bin_name) - } else if line.starts_with("Exec=") { - format!("Exec={}/{}", &*BIN_PATH.to_string_lossy(), bin_name) - } else if line.starts_with("TryExec=") { - format!("TryExec={}/{}", &*BIN_PATH.to_string_lossy(), bin_name) - } else { - line.to_string() - } - }) - .collect::>() - .join("\n"); - - let mut writer = BufWriter::new(File::create(output_path)?); - writer.write_all(processed_content.as_bytes())?; - - let final_path = data_path - .join("applications") - .join(format!("{name}-soar.desktop")); - - if let Some(parent) = final_path.parent() { - fs::create_dir_all(parent).await.context(anyhow::anyhow!( - "Failed to create desktop files directory at {}", - parent.to_string_lossy().color(Color::Blue) - ))?; - } - - create_symlink(output_path, &final_path).await?; - Ok(()) -} - -pub async fn integrate_using_remote_files(package: &Package, file_path: &Path) -> Result<()> { - let home_data = home_data_path(); - let data_path = Path::new(&home_data); - - let icon_output_path = file_path.with_extension("png"); - let desktop_output_path = file_path.with_extension("desktop"); - - let icon_url = &package.icon; - let desktop_url = &package.desktop; - - let icon_content = download(icon_url, "image", false).await?; - - fs::write(&icon_output_path, &icon_content).await?; - - let desktop_content = if let Some(desktop_url) = desktop_url { - match download(desktop_url, "desktop file", false).await { - Ok(content) => Some(content), - Err(_) => None, - } - } else { - None - }; - - let desktop_content = match desktop_content { - Some(content) => content, - None => create_default_desktop_entry( - &package.pkg_name, - &package.pkg, - &package.category.replace(',', ";"), - ), - }; - - fs::write(&desktop_output_path, &desktop_content).await?; - - try_join!( - process_icon(&icon_output_path, &package.pkg_name, data_path), - process_desktop( - &desktop_output_path, - &package.pkg_name, - &package.pkg, - data_path - ) - )?; - - Ok(()) -} - -fn create_default_desktop_entry(bin_name: &str, name: &str, categories: &str) -> Vec { - format!( - "[Desktop Entry]\n\ - Type=Application\n\ - Name={}\n\ - Icon={}\n\ - Exec={}\n\ - Categories={};\n", - name, bin_name, bin_name, categories - ) - .as_bytes() - .to_vec() -} - -pub async fn setup_portable_dir( - bin_name: &str, - package_path: &Path, - portable: Option, - portable_home: Option, - portable_config: Option, -) -> Result<()> { - let pkg_config = package_path.with_extension("config"); - let pkg_home = package_path.with_extension("home"); - - let (portable_home, portable_config) = if let Some(portable) = portable { - (Some(portable.clone()), Some(portable.clone())) - } else { - (portable_home, portable_config) - }; - - if let Some(portable_home) = portable_home { - if portable_home.is_empty() { - fs::create_dir(&pkg_home).await?; - } else { - let portable_home = PathBuf::from(portable_home) - .join(bin_name) - .with_extension("home"); - fs::create_dir_all(&portable_home) - .await - .context(anyhow::anyhow!( - "Failed to create or access directory at {}", - &portable_home.to_string_lossy().color(Color::Blue) - ))?; - create_symlink(&portable_home, &pkg_home).await?; - } - } - if let Some(portable_config) = portable_config { - if portable_config.is_empty() { - fs::create_dir(&pkg_config).await?; - } else { - let portable_config = PathBuf::from(portable_config) - .join(bin_name) - .with_extension("config"); - fs::create_dir_all(&portable_config) - .await - .context(anyhow::anyhow!( - "Failed to create or access directory at {}", - &portable_config.to_string_lossy().color(Color::Blue) - ))?; - create_symlink(&portable_config, &pkg_config).await?; - } - } - - Ok(()) -} diff --git a/src/package/build.rs b/src/package/build.rs deleted file mode 100644 index aad80a6..0000000 --- a/src/package/build.rs +++ /dev/null @@ -1,213 +0,0 @@ -use anyhow::Result; -use chrono::Utc; -use nanoid::nanoid; -use std::{ - fs::{self, File, Permissions}, - io::{BufRead, BufReader, BufWriter, Write}, - os::unix::fs::PermissionsExt, - path::{Path, PathBuf}, - process::{Command, Stdio}, - sync::mpsc, -}; -use tracing::{debug, error, info}; - -use crate::{ - core::{ - color::{Color, ColorExt}, - constant::CACHE_PATH, - }, - misc::download::download, -}; - -#[derive(Debug)] -pub struct BuildOutput { - sbuild_successful: bool, - sbuild_pkg: String, - pkg_ver: String, - pkg_type: String, - sbuild_outdir: PathBuf, - sbuild_tmpdir: PathBuf, -} - -impl BuildOutput { - pub async fn from(log_path: &Path, vars: &[(String, String)]) -> Result { - let mut output = BuildOutput { - sbuild_successful: false, - sbuild_pkg: String::new(), - pkg_ver: String::new(), - pkg_type: String::new(), - sbuild_outdir: PathBuf::new(), - sbuild_tmpdir: PathBuf::new(), - }; - - for (key, value) in vars { - match key.as_str() { - "SBUILD_SUCCESSFUL" => { - output.sbuild_successful = value.to_lowercase() == "yes" - || value.to_lowercase() == "true" - || value == "1" - } - "SBUILD_PKG" => output.sbuild_pkg = value.to_string(), - "PKG_VER" => output.pkg_ver = value.to_string(), - "PKG_TYPE" => output.pkg_type = value.to_string(), - "SBUILD_OUTDIR" => output.sbuild_outdir = PathBuf::from(value), - "SBUILD_TMPDIR" => output.sbuild_tmpdir = PathBuf::from(value), - _ => {} - } - } - - if output.sbuild_pkg.is_empty() { - anyhow::bail!("SBUILD_PKG not found in environment file"); - } - if output.pkg_ver.is_empty() { - anyhow::bail!("PKG_VER not found in environment file."); - } - if output.pkg_type.is_empty() { - anyhow::bail!("PKG_TYPE not found in environment file."); - } - if !output.sbuild_outdir.is_dir() { - anyhow::bail!("SBUILD_OUTDIR is invalid."); - } - if !output.sbuild_tmpdir.is_dir() { - anyhow::bail!("SBUILD_TMPDIR is invalid."); - } - - std::fs::remove_dir_all(&output.sbuild_tmpdir)?; - let dir = std::fs::read_dir(&output.sbuild_outdir)?; - let final_dir = CACHE_PATH.join(&output.sbuild_pkg); - std::fs::create_dir_all(&final_dir)?; - - for entry in dir { - let file = entry?; - let file_name = file.file_name(); - let final_path = final_dir.join(&file_name); - std::fs::copy(file.path(), &final_path)?; - } - - std::fs::copy(log_path, final_dir.join(format!("build.log")))?; - std::fs::remove_file(log_path)?; - - std::fs::remove_dir_all(&output.sbuild_outdir)?; - - Ok(output) - } -} - -enum OutputLine { - Stdout(String), - Stderr(String), -} - -pub async fn init>(file_path: P) -> Result<()> { - let file_path = file_path.as_ref(); - let output_env_path = PathBuf::from(&format!("{}.env", file_path.display())); - - let sbuild_runner = if let Ok(sbuild_runner) = which::which("sbuild-runner") { - sbuild_runner - } else { - let runner_path = CACHE_PATH.join("sbuild-runner").to_path_buf(); - if !runner_path.exists() { - let runner_url = "https://raw.githubusercontent.com/pkgforge/soarpkgs/9fe521e47f4e265345e19526da2879654f268491/scripts/sbuild_runner.sh"; - download(runner_url, Some(runner_path.to_string_lossy().to_string())).await?; - fs::set_permissions(&runner_path, Permissions::from_mode(0o755))?; - } - runner_path - }; - - let sbuild_id = nanoid!(); - let mut child = Command::new(sbuild_runner) - .arg(file_path) - .env("SBUILD_ID", &sbuild_id) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - let stdout = child.stdout.take().unwrap(); - let stderr = child.stderr.take().unwrap(); - - let (tx, rx) = mpsc::channel(); - let tx_stderr = tx.clone(); - - let log_path = CACHE_PATH.join(&format!("{}.build.log", sbuild_id)); - let log_file = File::create(&log_path)?; - let mut writer = BufWriter::new(log_file); - - let stdout_handle = std::thread::spawn({ - move || { - let reader = BufReader::new(stdout); - reader.lines().for_each(|line| { - if let Ok(line) = line { - tx.send(OutputLine::Stdout(line)).unwrap(); - } - }); - } - }); - - let stderr_handle = std::thread::spawn({ - move || { - let reader = BufReader::new(stderr); - reader.lines().for_each(|line| { - if let Ok(line) = line { - tx_stderr.send(OutputLine::Stderr(line)).unwrap(); - } - }); - } - }); - - let output_handle = std::thread::spawn(move || { - let ts_format = "%FT%T%.3f"; - while let Ok(output) = rx.recv() { - let now = Utc::now().format(ts_format); - match output { - OutputLine::Stdout(line) => { - debug!("[{}] {}", now, line); - writeln!(writer, "[{}] {}", now, line).unwrap(); - } - OutputLine::Stderr(line) => { - debug!("[{}] ERR: {}", now, line); - writeln!(writer, "[{}] ERR: {}", now, line).unwrap(); - } - } - } - }); - - stdout_handle.join().unwrap(); - stderr_handle.join().unwrap(); - - output_handle.join().unwrap(); - - let status = child.wait()?; - if !status.success() { - anyhow::bail!("Build failed with status: {}", status); - } - - if output_env_path.exists() { - let f = File::open(output_env_path)?; - let reader = BufReader::new(f); - - let mut output = Vec::new(); - for line in reader.lines() { - let line = line?.trim().to_owned(); - - if line.is_empty() || line.starts_with('#') { - continue; - } - - if let Some((key, value)) = line.split_once('=') { - output.push(( - key.trim_matches([' ', '"', '\'']).to_owned(), - value.trim_matches([' ', '"', '\'']).to_owned(), - )); - } - } - - let final_output = BuildOutput::from(&log_path, &output).await?; - if final_output.sbuild_successful { - info!("{}", "Build successful.".color(Color::BrightGreen)); - } else { - error!("{}", "Build failed.".color(Color::BrightRed)); - } - } - - Ok(()) -} diff --git a/src/package/image.rs b/src/package/image.rs deleted file mode 100644 index 65233e5..0000000 --- a/src/package/image.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::io::{self, Cursor, Read, Write}; - -use anyhow::Result; -use base64::{engine::general_purpose, Engine}; -use icy_sixel::{ - sixel_string, DiffusionMethod, MethodForLargest, MethodForRep, PixelFormat, Quality, -}; -use image::{DynamicImage, GenericImageView, ImageFormat, Rgba}; -use termion::raw::IntoRawMode; -use tokio::fs; - -use crate::core::{ - constant::REGISTRY_PATH, - util::{download, get_font_height, get_font_width}, -}; - -use super::ResolvedPackage; - -fn is_kitty_supported() -> Result { - let mut stdout = io::stdout().into_raw_mode()?; - let sequence = "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c"; - - write!(stdout, "{}", sequence)?; - stdout.flush()?; - - let mut buffer = [0u8; 1024]; - let mut stdin = io::stdin(); - - if let Ok(bytes_read) = stdin.read(&mut buffer) { - if bytes_read > 0 { - let buf_str = String::from_utf8_lossy(&buffer); - return Ok(buf_str.contains("OK")); - } - } - - Ok(false) -} - -fn is_sixel_supported() -> Result { - let mut stdout = io::stdout().into_raw_mode()?; - let sequence = "\x1b[c"; - - write!(stdout, "{}", sequence)?; - stdout.flush()?; - - let mut buffer = Vec::new(); - let stdin = io::stdin(); - - for byte in stdin.bytes() { - let byte = byte?; - if byte == b'c' { - break; - } - buffer.push(byte); - } - - let buf_str = String::from_utf8_lossy(&buffer); - for code in buf_str.split([';', '?']) { - if code == "4" { - return Ok(true); - } - } - Ok(false) -} - -fn build_transmit_sequence(base64_data: &str) -> String { - let chunk_size = 4096; - let mut pos = 0; - let mut sequence = String::new(); - - while pos < base64_data.len() { - sequence.push_str("\x1b_G"); - - if pos == 0 { - sequence.push_str("a=T,f=100,"); - } - - let end = std::cmp::min(pos + chunk_size, base64_data.len()); - let chunk = &base64_data[pos..end]; - pos = end; - - if pos < base64_data.len() { - sequence.push_str("m=1"); - } - - if !chunk.is_empty() { - sequence.push(';'); - sequence.push_str(chunk); - } - sequence.push_str("\x1b\\"); - } - - sequence.push('\n'); - sequence -} - -async fn halfblock_string(img: &DynamicImage) -> String { - let upper_half = '▀'; - let lower_half = '▄'; - let (width, height) = img.dimensions(); - let img_buffer = img.to_rgba8(); - let mut output = String::with_capacity((width * height * 20) as usize); - - let blend_alpha = |pixel: &Rgba| -> Rgba { - if pixel[3] == 255 { - *pixel - } else { - let alpha = pixel[3] as f32 / 255.0; - Rgba([ - (pixel[0] as f32 * alpha) as u8, - (pixel[1] as f32 * alpha) as u8, - (pixel[2] as f32 * alpha) as u8, - 255, - ]) - } - }; - - let pixel_to_ansi_fg = |pixel: &Rgba| -> String { - format!("\x1b[38;2;{};{};{}m", pixel[0], pixel[1], pixel[2]) - }; - - let pixel_to_ansi_bg = |pixel: &Rgba| -> String { - format!("\x1b[48;2;{};{};{}m", pixel[0], pixel[1], pixel[2]) - }; - - let is_transparent = |pixel: &Rgba| -> bool { - pixel[3] < 25 // Consider pixels with very low alpha as fully transparent - }; - - for y in (0..height).step_by(2) { - for x in 0..width { - let top_pixel = img_buffer.get_pixel(x, y); - - if y + 1 >= height { - // Last row for odd-height images - if is_transparent(top_pixel) { - output.push(' '); - } else { - output.push_str(&pixel_to_ansi_fg(&blend_alpha(top_pixel))); - output.push(upper_half); - output.push_str("\x1b[0m"); - } - continue; - } - - let bottom_pixel = img_buffer.get_pixel(x, y + 1); - match (is_transparent(top_pixel), is_transparent(bottom_pixel)) { - (true, true) => output.push(' '), // Both transparent - (true, false) => { - // Only top pixel visible - output.push_str(&pixel_to_ansi_fg(&blend_alpha(bottom_pixel))); - output.push(lower_half); - output.push_str("\x1b[0m"); - } - (false, true) => { - // Only bottom pixel visible - output.push_str(&pixel_to_ansi_fg(&blend_alpha(top_pixel))); - output.push(upper_half); - output.push_str("\x1b[0m"); - } - (false, false) => { - // Both pixels visible - let top = blend_alpha(top_pixel); - let bottom = blend_alpha(bottom_pixel); - output.push_str(&pixel_to_ansi_fg(&bottom)); - output.push_str(&pixel_to_ansi_bg(&top)); - output.push(lower_half); - output.push_str("\x1b[0m"); - } - } - } - output.push('\n'); - } - - output -} - -pub async fn load_default_icon(icon_path: &str) -> Result> { - let icon_path = REGISTRY_PATH.join("icons").join(icon_path); - let content = if icon_path.exists() { - fs::read(&icon_path).await? - } else { - vec![] - }; - Ok(content) -} - -pub async fn get_package_image_string(resolved_package: &ResolvedPackage) -> String { - let package = &resolved_package.package; - let icon = download(&package.icon, "icon", true).await; - let icon = match icon { - Ok(icon) => icon, - Err(_) => load_default_icon(&format!( - "{}-{}.png", - resolved_package.repo_name, resolved_package.collection - )) - .await - .unwrap_or_default(), - }; - - let image_width = (get_font_width() * 30) as u32; - let image_height = (get_font_height() * 16) as u32; - - let img = match image::load_from_memory(&icon) { - Ok(img) => img, - Err(_) => image::load_from_memory( - &load_default_icon(&format!( - "{}-{}.png", - resolved_package.repo_name, resolved_package.collection - )) - .await - .unwrap_or_default(), - ) - .unwrap(), - }; - - if is_kitty_supported().unwrap_or(false) { - let img = img.resize_exact( - image_width, - image_height, - image::imageops::FilterType::Lanczos3, - ); - - let mut icon = Vec::new(); - let mut cursor = Cursor::new(&mut icon); - img.write_to(&mut cursor, ImageFormat::Png) - .expect("Failed to write image in PNG format"); - - let encoded = general_purpose::STANDARD.encode(&icon); - - return build_transmit_sequence(&encoded); - } else if is_sixel_supported().unwrap_or(false) { - let img = img.resize_exact( - image_width, - image_height, - image::imageops::FilterType::Lanczos3, - ); - - let (width, height) = img.dimensions(); - let img_rgba8 = img.to_rgba8(); - let bytes = img_rgba8.as_raw(); - - let sixel_output = sixel_string( - bytes, - width as i32, - height as i32, - PixelFormat::RGBA8888, - DiffusionMethod::Stucki, - MethodForLargest::Auto, - MethodForRep::Auto, - Quality::HIGH, - ) - .unwrap(); - let sixel_output = sixel_output.replace("\x1BPq", "\x1BP0;1q"); - - return sixel_output; - }; - - let img = img.resize_exact(30, 30, image::imageops::FilterType::Lanczos3); - halfblock_string(&img).await -} diff --git a/src/package/install.rs b/src/package/install.rs deleted file mode 100644 index 91d3f8b..0000000 --- a/src/package/install.rs +++ /dev/null @@ -1,385 +0,0 @@ -use std::{ - fs::{File, Permissions}, - io::BufReader, - os::unix::fs::PermissionsExt, - path::{Path, PathBuf}, - sync::Arc, -}; - -use anyhow::{Context, Result}; -use futures::StreamExt; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use reqwest::Url; -use tokio::{ - fs, - io::{AsyncReadExt, AsyncWriteExt}, - sync::Mutex, -}; -use tracing::info; - -use crate::{ - core::{ - color::{Color, ColorExt}, - constant::{BIN_PATH, PACKAGES_PATH}, - file::{get_file_type, FileType}, - util::{calculate_checksum, download_progress_style, validate_checksum}, - }, - registry::installed::InstalledPackages, -}; - -use super::{ - appimage::{integrate_appimage, integrate_using_remote_files, setup_portable_dir}, - ResolvedPackage, -}; - -pub struct Installer { - resolved_package: ResolvedPackage, - install_path: PathBuf, - temp_path: PathBuf, -} - -impl Installer { - pub fn new(package: &ResolvedPackage) -> Self { - let temp_path = PACKAGES_PATH - .join("tmp") - .join(package.package.full_name('-')) - .with_extension("part"); - Self { - resolved_package: package.to_owned(), - install_path: Path::new("").to_path_buf(), - temp_path, - } - } - - pub async fn execute( - &mut self, - idx: usize, - total: usize, - installed_packages: Arc>, - portable: Option, - portable_home: Option, - portable_config: Option, - multi_progress: Option>, - ) -> Result<()> { - let package = &self.resolved_package.package; - - let prefix = format!( - "[{}/{}] {}", - (idx + 1).color(Color::Green), - total.color(Color::Cyan), - package.full_name('/').color(Color::BrightBlue) - ); - - if let Some(parent) = self.temp_path.parent() { - fs::create_dir_all(parent).await.context(format!( - "{}: Failed to create temp directory {}", - prefix, - self.temp_path.to_string_lossy().color(Color::Blue) - ))?; - } - - if Url::parse(&package.download_url).is_ok() { - self.download_remote_package(multi_progress.clone(), &prefix) - .await?; - } else { - self.copy_local_package(multi_progress.clone(), &prefix) - .await?; - } - - let checksum = calculate_checksum(&self.temp_path).await?; - - self.install_path = package.get_install_path(&checksum); - if let Some(parent) = self.install_path.parent() { - fs::create_dir_all(parent).await.context(format!( - "{}: Failed to create install directory {}", - prefix, - self.install_path.to_string_lossy().color(Color::Blue) - ))?; - } - - self.save_file().await?; - self.symlink_bin().await?; - - let mut file = BufReader::new(File::open(&self.install_path)?); - let file_type = get_file_type(&mut file); - - let warn_bar = if let Some(ref mp) = multi_progress { - let warn_bar = mp.insert(1, ProgressBar::new(0)); - warn_bar.set_style(ProgressStyle::default_bar().template("{msg}").unwrap()); - Some(warn_bar) - } else { - None - }; - match file_type { - FileType::AppImage => { - if integrate_appimage(&mut file, package, &self.install_path) - .await - .is_ok() - { - setup_portable_dir( - &package.pkg_name, - &self.install_path, - portable, - portable_home, - portable_config, - ) - .await?; - } else if let Some(wb) = warn_bar { - wb.finish_with_message(format!( - "{}: {}", - prefix, - "Failed to integrate AppImage".color(Color::BrightYellow) - )); - }; - } - FileType::FlatImage => { - if integrate_using_remote_files(package, &self.install_path) - .await - .is_ok() - { - setup_portable_dir( - &package.pkg_name, - Path::new(&format!( - "{}/.{}", - self.install_path.parent().unwrap().display(), - package.pkg_name - )), - None, - None, - portable_config, - ) - .await?; - } else if let Some(wb) = warn_bar { - wb.finish_with_message(format!( - "{}: {}", - prefix, - "Failed to integrate FlatImage".color(Color::BrightYellow) - )); - }; - } - _ => {} - } - - { - let mut installed_packages = installed_packages.lock().await; - installed_packages - .register_package(&self.resolved_package, &checksum) - .await?; - } - - if let Some(mp) = multi_progress { - let installed_progress = mp.insert_from_back(1, ProgressBar::new(0)); - installed_progress.set_style(ProgressStyle::default_bar().template("{msg}").unwrap()); - installed_progress.finish_with_message(format!( - "[{}/{}] Installed {}", - (idx + 1).color(Color::Green), - total.color(Color::Cyan), - package.full_name('/').color(Color::Blue) - )); - } - - if !package.note.is_empty() { - info!( - "{}: {}", - prefix, - package - .note - .replace("
", "\n ") - .color(Color::BrightYellow) - ); - } - - Ok(()) - } - - async fn download_remote_package( - &self, - multi_progress: Option>, - prefix: &str, - ) -> Result<()> { - let prefix = prefix.to_owned(); - let package = &self.resolved_package.package; - let temp_path = &self.temp_path; - let client = reqwest::Client::new(); - let downloaded_bytes = if temp_path.exists() { - let meta = fs::metadata(&temp_path).await?; - meta.len() - } else { - 0 - }; - - let response = client - .get(&package.download_url) - .header("Range", format!("bytes={}-", downloaded_bytes)) - .send() - .await - .context(format!("{}: Failed to download package", prefix))?; - let total_size = response - .content_length() - .map(|cl| cl + downloaded_bytes) - .unwrap_or(0); - - let download_progress = if let Some(ref mp) = multi_progress { - let download_progress = mp.insert_from_back(1, ProgressBar::new(0)); - - download_progress.set_style(download_progress_style(true)); - - download_progress.set_length(total_size); - download_progress.set_message(prefix.clone()); - Some(download_progress) - } else { - None - }; - - if !response.status().is_success() { - return Err(anyhow::anyhow!( - "{} Download failed {:?}", - prefix, - response.status().color(Color::Red), - )); - } - - let mut file = fs::OpenOptions::new() - .create(true) - .append(true) - .open(&temp_path) - .await - .context(format!("{}: Failed to open temp file for writing", prefix))?; - let mut stream = response.bytes_stream(); - - while let Some(chunk) = stream.next().await { - let chunk = chunk.context(format!("{}: Failed to read chunk", prefix))?; - file.write_all(&chunk).await?; - if let Some(ref pb) = download_progress { - pb.inc(chunk.len() as u64); - } - } - if let Some(ref pb) = download_progress { - pb.finish(); - } - file.flush().await?; - - let warn_bar = if let Some(mp) = multi_progress { - let warn_bar = mp.insert_from_back(1, ProgressBar::new(0)); - warn_bar.set_style(ProgressStyle::default_bar().template("{msg}").unwrap()); - Some(warn_bar) - } else { - None - }; - if package.bsum == "null" { - if let Some(wb) = warn_bar { - wb.finish_with_message(format!( - "{}: {}", - prefix, - "Missing checksum. Installing anyway.".color(Color::BrightYellow) - )); - } - } else { - let result = validate_checksum(&package.bsum, &self.temp_path).await; - if result.is_err() { - if let Some(wb) = warn_bar { - wb.finish_with_message(format!( - "{}: {}", - prefix, - "Checksum verification failed. Installing anyway." - .color(Color::BrightYellow) - )); - } - } - } - - Ok(()) - } - - async fn copy_local_package( - &self, - multi_progress: Option>, - prefix: &str, - ) -> Result<()> { - let temp_path = &self.temp_path; - let prefix = prefix.to_owned(); - let package = &self.resolved_package.package; - - let download_progress = if let Some(mp) = multi_progress { - let download_progress = mp.insert_from_back(1, ProgressBar::new(0)); - download_progress.set_style(download_progress_style(true)); - - let total_size = package.size.parse::().unwrap_or_default(); - download_progress.set_length(total_size); - download_progress.set_message(prefix.clone()); - Some(download_progress) - } else { - None - }; - - let mut file = fs::OpenOptions::new() - .create(true) - .append(true) - .open(&temp_path) - .await - .context(format!("{}: Failed to open temp file for writing", prefix))?; - let mut source = fs::File::open(&package.download_url).await?; - let mut buffer = vec![0u8; 8096]; - - while let Ok(n) = source.read(&mut buffer).await { - if n == 0 { - break; - } - - file.write_all(&buffer[..n]).await?; - if let Some(ref pb) = download_progress { - pb.inc(n as u64); - } - } - if let Some(pb) = download_progress { - pb.finish(); - } - file.flush().await?; - - Ok(()) - } - - async fn save_file(&self) -> Result<()> { - let install_path = &self.install_path; - let temp_path = &self.temp_path; - if install_path.exists() { - tokio::fs::remove_file(&install_path).await?; - } - tokio::fs::rename(&temp_path, &install_path).await?; - tokio::fs::set_permissions(&install_path, Permissions::from_mode(0o755)).await?; - xattr::set(install_path, "user.managed_by", b"soar")?; - - Ok(()) - } - - async fn symlink_bin(&self) -> Result<()> { - let package = &self.resolved_package.package; - let install_path = &self.install_path; - let symlink_path = &BIN_PATH.join(&package.pkg_name); - if symlink_path.exists() { - if let Ok(link) = symlink_path.read_link() { - if *install_path == link { - return Ok(()); - } - if let Ok(parent) = link.strip_prefix(&*PACKAGES_PATH) { - let package_path = parent.parent().unwrap().to_string_lossy(); - let package_name = &package_path[..8]; - - if package_name == package.full_name('-') { - fs::remove_dir_all(link.parent().unwrap()).await?; - } - }; - } - fs::remove_file(symlink_path).await?; - } - fs::symlink(&install_path, &symlink_path) - .await - .context(format!( - "Failed to link {} to {}", - install_path.to_string_lossy(), - symlink_path.to_string_lossy() - ))?; - - Ok(()) - } -} diff --git a/src/package/mod.rs b/src/package/mod.rs deleted file mode 100644 index 29bc5a1..0000000 --- a/src/package/mod.rs +++ /dev/null @@ -1,139 +0,0 @@ -mod appimage; -pub mod build; -pub mod image; -mod install; -pub mod remove; -pub mod run; -pub mod update; - -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use anyhow::Result; -use indicatif::MultiProgress; -use install::Installer; -use serde::{Deserialize, Serialize}; -use tokio::sync::Mutex; - -use crate::{core::constant::PACKAGES_PATH, registry::installed::InstalledPackages}; - -#[derive(Debug, Default, Clone, Deserialize, Serialize)] -pub struct Package { - pub pkg: String, - pub pkg_name: String, - pub description: String, - pub note: String, - pub version: String, - pub download_url: String, - pub size: String, - pub bsum: String, - pub build_date: String, - pub src_url: String, - pub homepage: String, - pub build_script: String, - pub build_log: String, - pub category: String, - pub provides: String, - pub icon: String, - pub desktop: Option, - pub pkg_id: Option, - pub family: Option, -} - -#[derive(Default, Debug, Clone)] -pub struct ResolvedPackage { - pub repo_name: String, - pub collection: String, - pub package: Package, -} - -impl ResolvedPackage { - pub async fn install( - &self, - idx: usize, - total: usize, - installed_packages: Arc>, - portable: Option, - portable_home: Option, - portable_config: Option, - multi_progress: Option>, - ) -> Result<()> { - let mut installer = Installer::new(self); - installer - .execute( - idx, - total, - installed_packages, - portable, - portable_home, - portable_config, - multi_progress, - ) - .await?; - Ok(()) - } -} - -impl Package { - pub fn get_install_dir(&self, checksum: &str) -> PathBuf { - PACKAGES_PATH.join(format!("{}-{}", &checksum[..8], self.full_name('-'))) - } - - pub fn get_install_path(&self, checksum: &str) -> PathBuf { - self.get_install_dir(checksum).join(&self.pkg_name) - } - - pub fn full_name(&self, join_char: char) -> String { - let family_prefix = self - .family - .to_owned() - .map(|family| format!("{}{}", family, join_char)) - .unwrap_or_default(); - format!("{}{}", family_prefix, self.pkg) - } -} - -#[derive(Debug)] -pub struct PackageQuery { - pub name: String, - pub family: Option, - pub collection: Option, -} - -pub fn parse_package_query(query: &str) -> PackageQuery { - let query = query.to_lowercase(); - let (base_query, collection) = query - .rsplit_once('#') - .map(|(n, r)| (n.to_owned(), (!r.is_empty()).then(|| r.to_lowercase()))) - .unwrap_or((query.to_owned(), None)); - - let (name, family) = base_query - .split_once('/') - .map(|(v, n)| (n.to_owned(), Some(v.to_owned()))) - .unwrap_or((base_query, None)); - - PackageQuery { - name, - family, - collection, - } -} - -#[inline] -pub fn gen_package_info(name: &str, path: &Path, size: u64) -> ResolvedPackage { - let package = Package { - pkg: name.to_owned(), - pkg_name: name.to_owned(), - size: size.to_string(), - download_url: path.to_string_lossy().to_string(), - ..Default::default() - }; - - ResolvedPackage { - repo_name: "local".to_owned(), - collection: "local".to_string(), - package, - } -} diff --git a/src/package/remove.rs b/src/package/remove.rs deleted file mode 100644 index 1422c39..0000000 --- a/src/package/remove.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::path::Path; - -use anyhow::{Context, Result}; -use tokio::fs; -use tracing::info; - -use crate::{ - core::{ - color::{Color, ColorExt}, - constant::BIN_PATH, - }, - package::appimage::remove_applinks, - registry::installed::{InstalledPackage, InstalledPackages}, -}; - -pub struct Remover { - package: InstalledPackage, -} - -impl Remover { - pub async fn new(package: &InstalledPackage) -> Result { - Ok(Self { - package: package.clone(), - }) - } - - pub async fn execute(&self, installed_packages: &mut InstalledPackages) -> Result<()> { - let package = &self.package; - - let install_dir = package.get_install_dir(); - let install_path = package.get_install_path(); - self.remove_symlink(&install_path).await?; - remove_applinks(&package.name, &package.bin_name, &install_path).await?; - self.remove_package_path(&install_dir).await?; - installed_packages.unregister_package(&self.package).await?; - - info!( - "Package {} removed successfully.", - package.full_name('/').color(Color::Blue) - ); - - Ok(()) - } - - pub async fn remove_symlink(&self, install_path: &Path) -> Result<()> { - let package = &self.package; - let symlink_path = BIN_PATH.join(&package.bin_name); - if symlink_path.exists() { - let target = fs::read_link(&symlink_path).await?; - if target == install_path { - fs::remove_file(&symlink_path).await?; - } - } - - Ok(()) - } - - pub async fn remove_package_path(&self, install_dir: &Path) -> Result<()> { - if install_dir.exists() { - fs::remove_dir_all(&install_dir).await.context(format!( - "Failed to remove package file: {}", - install_dir.to_string_lossy().color(Color::Blue) - ))?; - } - - Ok(()) - } -} diff --git a/src/package/run.rs b/src/package/run.rs deleted file mode 100644 index e9f9935..0000000 --- a/src/package/run.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::PathBuf, process::Command}; - -use anyhow::{Context, Result}; -use futures::StreamExt; -use tokio::{fs, io::AsyncWriteExt}; -use tracing::{error, info, warn}; - -use crate::core::{ - color::{Color, ColorExt}, - util::{format_bytes, validate_checksum}, -}; - -use super::ResolvedPackage; - -pub struct Runner { - args: Vec, - resolved_package: ResolvedPackage, - install_path: PathBuf, - temp_path: PathBuf, -} - -impl Runner { - pub fn new(package: &ResolvedPackage, install_path: PathBuf, args: &[String]) -> Self { - let temp_path = install_path.with_extension("part"); - Self { - args: args.to_owned(), - resolved_package: package.to_owned(), - install_path, - temp_path, - } - } - - pub async fn execute(&self) -> Result<()> { - let package = &self.resolved_package.package; - let package_name = &package.full_name('/'); - - if self.install_path.exists() { - if xattr::get(&self.install_path, "user.managed_by")?.as_deref() != Some(b"soar") { - return Err(anyhow::anyhow!( - "Path {} is not managed by soar. Exiting.", - self.install_path.to_string_lossy().color(Color::Blue) - )); - } else { - info!( - "Found existing cache for {}", - package_name.color(Color::Blue) - ); - return self.run().await; - } - } - - let client = reqwest::Client::new(); - let downloaded_bytes = if self.temp_path.exists() { - let meta = fs::metadata(&self.temp_path).await?; - meta.len() - } else { - 0 - }; - - let response = client - .get(&package.download_url) - .header("Range", format!("bytes={}-", downloaded_bytes)) - .send() - .await - .context(format!("{} Failed to download package", package_name))?; - let total_size = response - .content_length() - .map(|cl| cl + downloaded_bytes) - .unwrap_or(0); - println!( - "{}: Downloading package [{}]", - package_name.color(Color::Blue), - format_bytes(total_size).color(Color::Yellow) - ); - - if !response.status().is_success() { - return Err(anyhow::anyhow!( - "{}: Download failed {:?}", - package_name.color(Color::Blue), - response.status().color(Color::Red) - )); - } - - { - let mut file = fs::OpenOptions::new() - .write(true) - .create(true) - .append(true) - .open(&self.temp_path) - .await - .context(format!( - "{}: Failed to open temp file for writing", - package_name.color(Color::Blue) - ))?; - - let mut stream = response.bytes_stream(); - - while let Some(chunk) = stream.next().await { - let chunk = chunk.context(format!( - "{}: Failed to read chunk", - package_name.color(Color::Blue) - ))?; - file.write_all(&chunk).await?; - } - file.flush().await?; - } - - if package.bsum == "null" { - warn!( - "Missing checksum for {}. Installing anyway.", - package.full_name('/').color(Color::Blue) - ); - } else { - let result = validate_checksum(&package.bsum, &self.temp_path).await; - if result.is_err() { - error!( - "{}: Checksum verification failed.", - package_name.color(Color::Blue) - ); - } - } - - self.save_file().await?; - self.run().await?; - - Ok(()) - } - - async fn save_file(&self) -> Result<()> { - let install_path = &self.install_path; - let temp_path = &self.temp_path; - if install_path.exists() { - tokio::fs::remove_file(&install_path).await?; - } - tokio::fs::rename(&temp_path, &install_path).await?; - tokio::fs::set_permissions(&install_path, Permissions::from_mode(0o755)).await?; - xattr::set(install_path, "user.managed_by", b"soar")?; - - Ok(()) - } - - async fn run(&self) -> Result<()> { - Command::new(&self.install_path).args(&self.args).status()?; - - Ok(()) - } -} diff --git a/src/package/update.rs b/src/package/update.rs deleted file mode 100644 index 58b05fa..0000000 --- a/src/package/update.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use tracing::info; - -use crate::{ - core::color::{Color, ColorExt}, - error, - registry::PackageRegistry, -}; - -use super::{parse_package_query, PackageQuery, ResolvedPackage}; - -pub struct Updater { - package_names: Option>, -} - -impl Updater { - pub fn new(package_names: Option<&[String]>) -> Self { - Self { - package_names: package_names.map(|names| names.to_vec()), - } - } - - pub async fn execute(&self, registry: &PackageRegistry, quiet: bool) -> Result<()> { - let installed_guard = registry.installed_packages.lock().await; - let packages = match &self.package_names { - Some(r) => { - let resolved_packages: Result> = r - .iter() - .map(|package_name| registry.storage.resolve_package(package_name, true)) - .collect(); - resolved_packages? - } - None => installed_guard - .packages - .iter() - .filter_map(|installed| { - let pkg = parse_package_query(&installed.name); - let query = PackageQuery { - collection: Some(installed.collection.clone()), - ..pkg - }; - registry - .storage - .get_packages(&query) - .and_then(|v| v.into_iter().next()) - }) - .collect::>(), - }; - - let mut packages_to_update: Vec = Vec::new(); - - let multi_progress = Arc::new(MultiProgress::new()); - for package in packages { - if let Some(installed_package) = installed_guard - .packages - .iter() - .find(|installed| installed.full_name('-') == package.package.full_name('-')) - { - if installed_package.checksum != package.package.bsum { - packages_to_update.push(package); - } - } else { - error!( - "Package {} is not installed.", - package.package.full_name('/').color(Color::Blue) - ); - } - } - - drop(installed_guard); - - let total_progress_bar = if !quiet { - Some(multi_progress.add(ProgressBar::new(packages_to_update.len() as u64))) - } else { - None - }; - - if let Some(pb) = &total_progress_bar { - pb.set_style(ProgressStyle::with_template("Updating {pos}/{len}").unwrap()); - } - - if packages_to_update.is_empty() { - error!("No updates available"); - } else { - let mut update_count = 0; - for (idx, package) in packages_to_update.iter().enumerate() { - package - .install( - idx, - packages_to_update.len(), - registry.installed_packages.clone(), - None, - None, - None, - if quiet { - None - } else { - Some(multi_progress.clone()) - }, - ) - .await?; - update_count += 1; - if let Some(ref pb) = total_progress_bar { - pb.inc(1); - } - } - - if let Some(pb) = total_progress_bar { - pb.finish_and_clear(); - } - info!( - "{} packages updated.", - update_count.color(Color::BrightMagenta) - ); - } - - Ok(()) - } -} diff --git a/src/registry/fetcher.rs b/src/registry/fetcher.rs deleted file mode 100644 index 3241eff..0000000 --- a/src/registry/fetcher.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::{collections::HashMap, env::consts::ARCH, path::PathBuf}; - -use anyhow::{Context, Result}; -use futures::future::try_join_all; -use serde::Deserialize; -use tokio::fs; - -use crate::{ - core::{ - color::{Color, ColorExt}, - config::Repository, - constant::REGISTRY_PATH, - util::download, - }, - package::Package, -}; - -pub struct MetadataFetcher; - -#[derive(Deserialize)] -struct RepositoryResponse { - #[serde(flatten)] - collection: HashMap>, -} - -impl MetadataFetcher { - pub fn new() -> Self { - Self - } - - pub async fn execute(&self, repository: &Repository) -> Result> { - let url = format!( - "{}/{}", - repository.url, - repository - .metadata - .to_owned() - .unwrap_or("metadata.json".to_owned()) - ); - - let content = download(&url, "metadata", false).await?; - - let parsed: RepositoryResponse = - serde_json::from_slice(&content).context("Failed to parse metadata json")?; - - let metadata: HashMap>> = parsed - .collection - .iter() - .map(|(key, packages)| { - let package_map: HashMap> = - packages.iter().fold(HashMap::new(), |mut acc, package| { - acc.entry(package.pkg.to_lowercase().clone()) - .or_default() - .push(Package { - family: package - .download_url - .split('/') - .rev() - .nth(1) - .map(|v| v.to_owned()) - .filter(|v| v != ARCH), - ..package.clone() - }); - acc - }); - - (key.clone(), package_map) - }) - .collect(); - - let content = rmp_serde::to_vec(&metadata) - .context("Failed to serialize package metadata to MessagePack")?; - - let path = repository.get_path(); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .await - .context("Failed to create registry directory")?; - } - - fs::write(&path, &content).await.with_context(|| { - format!( - "Failed to write metadata for {}", - repository.name.clone().color(Color::Yellow) - ) - })?; - - let _ = self.fetch_icons(repository).await; - - Ok(content) - } - - async fn fetch_icon( - &self, - icon_path: PathBuf, - base_url: String, - key: &str, - ) -> Result)>> { - if fs::metadata(&icon_path).await.is_ok() { - Ok(None) - } else { - let content = download(&base_url, "icon", true).await?; - Ok(Some((key.to_owned(), content))) // Return the key and icon data if downloaded - } - } - - pub async fn fetch_icons(&self, repository: &Repository) -> Result<()> { - // fetch default icons - let icon_futures: Vec<_> = repository - .sources - .iter() - .map(|(key, base_url)| { - let base_url = format!("{}/{}.default.png", base_url, key); - - let icon_path = REGISTRY_PATH - .join("icons") - .join(format!("{}-{}.png", repository.name, key)); - self.fetch_icon(icon_path, base_url, key) - }) - .collect(); - - let icons = try_join_all(icon_futures).await?; - let icons_to_save: Vec<_> = icons.into_iter().flatten().collect(); - - for (key, icon) in icons_to_save { - let icon_path = REGISTRY_PATH - .join("icons") - .join(format!("{}-{}.png", repository.name, key)); - - if let Some(parent) = icon_path.parent() { - fs::create_dir_all(parent).await.context(anyhow::anyhow!( - "Failed to create icon directory at {}", - parent.to_string_lossy().color(Color::Blue) - ))?; - } - - fs::write(icon_path, icon).await?; - } - - Ok(()) - } - - pub async fn checksum(&self, repository: &Repository) -> Result> { - let url = format!( - "{}/{}", - repository.url, - repository - .metadata - .to_owned() - .map(|file| format!("{file}.bsum")) - .unwrap_or("metadata.json.bsum".to_owned()) - ); - - let content = download(&url, "metadata", true).await?; - - Ok(content) - } -} diff --git a/src/registry/installed.rs b/src/registry/installed.rs deleted file mode 100644 index ea50694..0000000 --- a/src/registry/installed.rs +++ /dev/null @@ -1,266 +0,0 @@ -use std::{collections::HashMap, path::PathBuf}; - -use anyhow::{Context, Result}; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use tokio::fs; -use tracing::info; - -use crate::{ - core::{ - color::{Color, ColorExt}, - constant::{BIN_PATH, INSTALL_TRACK_PATH, PACKAGES_PATH}, - util::{format_bytes, parse_size}, - }, - package::{parse_package_query, remove::Remover, ResolvedPackage}, -}; - -use super::storage::PackageStorage; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct InstalledPackage { - pub repo_name: String, - pub collection: String, - pub name: String, - pub family: Option, - pub bin_name: String, - pub version: String, - pub checksum: String, - pub size: u64, - pub timestamp: DateTime, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct InstalledPackages { - pub packages: Vec, -} - -impl InstalledPackages { - pub async fn new() -> Result { - let path = INSTALL_TRACK_PATH.join("latest"); - - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .await - .context("Failed to create installs directory to track installations.")?; - } - - let packages = if path.exists() { - let content = tokio::fs::read(&path) - .await - .context("Failed to read installed packages")?; - - let mut de = rmp_serde::Deserializer::new(&content[..]); - - InstalledPackages::deserialize(&mut de)? - } else { - InstalledPackages { - packages: Vec::new(), - } - }; - - Ok(packages) - } - - pub fn is_installed(&self, package: &ResolvedPackage) -> bool { - self.packages - .iter() - .any(|installed| installed.full_name('-') == package.package.full_name('-')) - } - - fn find_package_mut(&mut self, package: &ResolvedPackage) -> Option<&mut InstalledPackage> { - self.packages - .iter_mut() - .find(|installed| installed.full_name('-') == package.package.full_name('-')) - } - - pub fn find_package(&self, package: &ResolvedPackage) -> Option<&InstalledPackage> { - self.packages.iter().find(|installed| { - installed.repo_name == package.repo_name - && installed.collection == package.collection - && installed.full_name('-') == package.package.full_name('-') - }) - } - - pub async fn register_package( - &mut self, - resolved_package: &ResolvedPackage, - checksum: &str, - ) -> Result<()> { - let package = resolved_package.package.to_owned(); - - let new_installed = InstalledPackage { - repo_name: resolved_package.repo_name.to_owned(), - collection: resolved_package.collection.to_string().to_owned(), - name: package.pkg, - family: package.family, - bin_name: package.pkg_name, - version: package.version, - checksum: checksum.to_owned(), - size: parse_size(&package.size).unwrap_or_default(), - timestamp: Utc::now(), - }; - - if let Some(installed) = self.find_package_mut(resolved_package) { - *installed = new_installed; - } else { - self.packages.push(new_installed); - } - - self.save().await?; - - Ok(()) - } - - pub async fn unregister_package(&mut self, installed_package: &InstalledPackage) -> Result<()> { - self.packages - .retain(|installed| installed.full_name('-') != installed_package.full_name('-')); - - self.save().await?; - - Ok(()) - } - - pub async fn remove(&mut self, installed_package: &InstalledPackage) -> Result<()> { - let remover = Remover::new(installed_package).await?; - remover.execute(self).await?; - Ok(()) - } - - pub async fn save(&self) -> Result<()> { - let path = INSTALL_TRACK_PATH.join("latest"); - - let content = rmp_serde::to_vec(&self) - .context("Failed to serialize installed packages to MessagePack")?; - - fs::write(&path, content).await.context(format!( - "Failed to write to {}", - path.to_string_lossy().color(Color::Red) - ))?; - - Ok(()) - } - - pub async fn info( - &self, - packages: Option<&[String]>, - package_store: &PackageStorage, - ) -> Result<()> { - let mut total: HashMap = HashMap::new(); - - let resolved_packages = packages - .map(|pkgs| { - pkgs.iter() - .flat_map(|package| { - let query = parse_package_query(package); - package_store - .get_packages(&query) - .unwrap_or_default() - .into_iter() - .filter_map(|package| self.find_package(&package).cloned()) - }) - .collect::>() - }) - .unwrap_or_else(|| self.packages.clone()); - - if resolved_packages.is_empty() { - return Err(anyhow::anyhow!("No installed packages")); - } - - resolved_packages.iter().for_each(|package| { - info!( - "- [{}] {1}:{1}-{2} ({3}) ({4})", - package.collection.clone().color(Color::BrightGreen), - package.name.clone().color(Color::Blue), - package.version.clone().color(Color::Green), - package - .timestamp - .format("%Y-%m-%d %H:%M:%S") - .color(Color::Yellow), - format_bytes(package.size).color(Color::Magenta) - ); - - let (count, size) = total.get(&package.collection).unwrap_or(&(0, 0)); - total.insert( - package.collection.to_owned(), - (count + 1, size + package.size), - ); - }); - info!("{:<2} Installed:", ""); - - for (collection, (count, size)) in total.iter() { - info!( - "{:<4} {}: {} ({})", - "", - collection, - count.color(Color::BrightGreen), - format_bytes(size.to_owned()) - ); - } - - let (count, value) = total - .values() - .fold((0, 0), |(count_acc, value_acc), &(count, value)| { - (count_acc + count, value_acc + value) - }); - - info!( - "{:<2} Total: {} ({})", - "", - count.color(Color::BrightYellow), - format_bytes(value) - ); - - Ok(()) - } - - pub async fn use_package(&self, resolved_package: &ResolvedPackage) -> Result<()> { - if let Some(installed) = self.find_package(resolved_package) { - let install_path = resolved_package - .package - .get_install_path(&installed.checksum); - let symlink_path = &BIN_PATH.join(&installed.bin_name); - - if symlink_path.exists() { - if xattr::get_deref(symlink_path, "user.managed_by")?.as_deref() != Some(b"soar") { - return Err(anyhow::anyhow!( - "{} is not managed by soar", - symlink_path.to_string_lossy().color(Color::Blue) - )); - } - fs::remove_file(symlink_path).await?; - } - - fs::symlink(&install_path, symlink_path) - .await - .context(format!( - "Failed to link {} to {}", - install_path.to_string_lossy().color(Color::Blue), - symlink_path.to_string_lossy().color(Color::Blue) - ))?; - } else { - return Err(anyhow::anyhow!("NOT_INSTALLED")); - } - - Ok(()) - } -} - -impl InstalledPackage { - pub fn full_name(&self, join_char: char) -> String { - let family_prefix = self - .family - .to_owned() - .map(|family| format!("{}{}", family, join_char)) - .unwrap_or_default(); - format!("{}{}", family_prefix, self.name) - } - - pub fn get_install_dir(&self) -> PathBuf { - PACKAGES_PATH.join(format!("{}-{}", &self.checksum[..8], self.full_name('-'))) - } - - pub fn get_install_path(&self) -> PathBuf { - self.get_install_dir().join(&self.bin_name) - } -} diff --git a/src/registry/loader.rs b/src/registry/loader.rs deleted file mode 100644 index 13f5419..0000000 --- a/src/registry/loader.rs +++ /dev/null @@ -1,38 +0,0 @@ -use anyhow::{Context, Result}; -use tokio::fs; -use tracing::warn; - -use crate::core::config::Repository; - -use super::fetcher::MetadataFetcher; - -pub struct MetadataLoader; - -impl MetadataLoader { - pub fn new() -> Self { - Self - } - - pub async fn execute(&self, repo: &Repository, fetcher: &MetadataFetcher) -> Result> { - let checksum = fetcher.checksum(repo).await; - - if let Ok(checksum) = checksum { - let checksum_path = repo - .get_path() - .with_file_name(format!("{}.remote.bsum", repo.name)); - let local_checksum = fs::read(&checksum_path).await.unwrap_or_default(); - if checksum != local_checksum { - warn!("Local registry is outdated. Refetching..."); - let content = fetcher.execute(repo).await; - fs::write(checksum_path, &checksum).await?; - return content; - } - } - - let path = repo.get_path(); - let content = fs::read(path) - .await - .context("Failed to load registry path.")?; - Ok(content) - } -} diff --git a/src/registry/mod.rs b/src/registry/mod.rs deleted file mode 100644 index 038a231..0000000 --- a/src/registry/mod.rs +++ /dev/null @@ -1,385 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use serde::Deserialize; -use termion::cursor; -use tokio::{fs, sync::Mutex}; -use tracing::{error, info}; - -use fetcher::MetadataFetcher; -use installed::InstalledPackages; -use loader::MetadataLoader; -use storage::{PackageStorage, RepositoryPackages}; - -use crate::{ - core::{ - color::{Color, ColorExt}, - config::CONFIG, - util::{get_terminal_width, interactive_ask, wrap_text, AskType}, - }, - package::{ - image::get_package_image_string, parse_package_query, update::Updater, ResolvedPackage, - }, -}; - -mod fetcher; -pub mod installed; -mod loader; -mod storage; - -pub struct PackageRegistry { - pub storage: PackageStorage, - pub installed_packages: Arc>, -} - -impl PackageRegistry { - pub async fn new() -> Result { - let loader = MetadataLoader::new(); - let fetcher = MetadataFetcher::new(); - let mut storage = PackageStorage::new(); - let installed_packages = Arc::new(Mutex::new(InstalledPackages::new().await?)); - - Self::load_or_fetch_packages(&loader, &fetcher, &mut storage).await?; - - Ok(Self { - storage, - installed_packages, - }) - } - - pub async fn load_or_fetch_packages( - loader: &MetadataLoader, - fetcher: &MetadataFetcher, - storage: &mut PackageStorage, - ) -> Result<()> { - for repo in &CONFIG.repositories { - let path = repo.get_path(); - let content = if path.exists() { - loader.execute(repo, fetcher).await? - } else { - let checksum = fetcher.checksum(repo).await?; - let checksum_path = repo - .get_path() - .with_file_name(format!("{}.remote.bsum", repo.name)); - fs::write(checksum_path, &checksum).await?; - - fetcher.execute(repo).await? - }; - - let mut de = rmp_serde::Deserializer::new(&content[..]); - let packages = match RepositoryPackages::deserialize(&mut de) { - Ok(packages) => packages, - Err(_) => { - error!("Metadata is invalid. Refetching..."); - let content = fetcher.execute(repo).await?; - let mut de = rmp_serde::Deserializer::new(&content[..]); - RepositoryPackages::deserialize(&mut de)? - } - }; - storage.add_repository(&repo.name, packages); - } - - Ok(()) - } - - pub async fn install_packages( - &self, - package_names: &[String], - force: bool, - portable: Option, - portable_home: Option, - portable_config: Option, - yes: bool, - quiet: bool, - ) -> Result<()> { - self.storage - .install_packages( - package_names, - force, - self.installed_packages.clone(), - portable, - portable_home, - portable_config, - yes, - quiet, - ) - .await - } - - pub async fn remove_packages(&self, package_names: &[String], exact: bool) -> Result<()> { - self.storage - .remove_packages(package_names, self.installed_packages.clone(), exact) - .await - } - - pub async fn search( - &self, - package_name: &str, - case_sensitive: bool, - limit: Option, - ) -> Result<()> { - let limit = limit.unwrap_or(CONFIG.search_limit.unwrap_or(20)); - let installed_guard = self.installed_packages.lock().await; - let result = self.storage.search(package_name, case_sensitive).await; - - if result.is_empty() { - Err(anyhow::anyhow!("No packages found")) - } else { - let displayed_results = result.iter().take(limit).collect::>(); - displayed_results.iter().for_each(|pkg| { - let installed = if installed_guard.is_installed(pkg) { - "+" - } else { - "-" - }; - info!( - "[{}] [{}] {}: {} ({})", - installed, - pkg.collection.clone().color(Color::BrightGreen), - pkg.package.full_name('/').color(Color::BrightBlue), - pkg.package.description, - pkg.package.size.clone().color(Color::BrightMagenta) - ); - }); - - if result.len() > limit { - info!( - "\x1b[5mShowing {} of {} results\x1b[0m", - limit, - result.len() - ); - } - Ok(()) - } - } - - pub async fn query(&self, package_name: &str) -> Result<()> { - let installed_guard = self.installed_packages.lock().await; - let query = parse_package_query(package_name); - let result = self.storage.get_packages(&query); - - let Some(result) = result else { - return Err(anyhow::anyhow!("No packages found")); - }; - - for pkg in result { - let installed_pkg = installed_guard.find_package(&pkg); - let package = &pkg.package; - - let formatted_name = format!( - "{} ({}#{})", - package.pkg_name.clone().color(Color::BrightGreen), - package.clone().full_name('/').color(Color::BrightCyan), - pkg.collection.clone().color(Color::BrightRed) - ); - let mut data: Vec<(&str, String)> = vec![ - ("Name", formatted_name), - ( - "Description", - package.description.clone().color(Color::BrightYellow), - ), - ( - "Homepage", - package.homepage.clone().color(Color::BrightBlue), - ), - ("Source", package.src_url.clone().color(Color::BrightBlue)), - ( - "Version", - package.version.clone().color(Color::BrightMagenta), - ), - ("Checksum", package.bsum.clone().color(Color::BrightMagenta)), - ("Size", package.size.clone().color(Color::BrightMagenta)), - ( - "Download URL", - package.download_url.clone().color(Color::BrightBlue), - ), - ( - "Build Date", - package.build_date.clone().color(Color::BrightMagenta), - ), - ( - "Build Log", - package.build_log.clone().color(Color::BrightBlue), - ), - ( - "Build Script", - package.build_script.clone().color(Color::BrightBlue), - ), - ("Note", package.note.clone().color(Color::BrightCyan)), - ( - "Category", - package.category.clone().color(Color::BrightCyan), - ), - ( - "Extra Bins", - package.provides.clone().color(Color::BrightBlack), - ), - ]; - - if let Some(installed) = installed_pkg { - data.push(( - "Install Path", - package - .clone() - .get_install_path(&installed.checksum) - .to_string_lossy() - .to_string() - .color(Color::BrightGreen), - )); - data.push(( - "Install Date", - installed - .timestamp - .format("%Y-%m-%d %H:%M:%S") - .to_string() - .color(Color::BrightMagenta), - )); - } - - let pkg_image = get_package_image_string(&pkg).await; - - let indent = 32; - - info!( - "{}{}{}", - pkg_image, - cursor::Up(15), - cursor::Right(indent).to_string() - ); - - data.iter().for_each(|(k, v)| { - let value = strip_ansi_escapes::strip_str(v); - - if !value.is_empty() && value != "null" { - let available_width = get_terminal_width() - indent as usize; - let line = wrap_text( - &format!("{}: {}", k.color(Color::Red).bold(), v), - available_width, - indent, - ); - - info!("{}{}", cursor::Right(indent).to_string(), line); - } - }); - - info!("{}", cursor::Down(1).to_string()); - } - Ok(()) - } - - pub async fn update(&self, package_names: Option<&[String]>, quiet: bool) -> Result<()> { - let updater = Updater::new(package_names); - updater.execute(self, quiet).await - } - - pub async fn info(&self, package_names: Option<&[String]>) -> Result<()> { - if let Some([package]) = package_names { - return self.query(package).await; - } - let installed_guard = self.installed_packages.lock().await; - installed_guard.info(package_names, &self.storage).await - } - - pub async fn list(&self, collection: Option<&str>) -> Result<()> { - let packages = self.storage.list_packages(collection); - if packages.is_empty() { - anyhow::bail!("No packages found"); - } - for resolved_package in packages { - let package = resolved_package.package.clone(); - let installed_guard = self.installed_packages.lock().await; - let install_prefix = if installed_guard.is_installed(&resolved_package) { - "+" - } else { - "-" - }; - info!( - "[{0}] [{1}] {2}:{3}-{4} ({5})", - install_prefix.color(Color::Red), - resolved_package.collection.color(Color::BrightGreen), - package.full_name('/').color(Color::Blue), - package.pkg.color(Color::Blue), - package.version.color(Color::Green), - package.size.color(Color::Magenta) - ); - } - Ok(()) - } - - pub async fn inspect(&self, package_name: &str, inspect_type: &str) -> Result<()> { - self.storage.inspect(package_name, inspect_type).await - } - - pub async fn run(&self, command: &[String], yes: bool) -> Result<()> { - self.storage.run(command, yes).await - } - - pub async fn use_package(&self, package_name: &str, quiet: bool) -> Result<()> { - let installed_guard = self.installed_packages.lock().await; - let resolved_package = self.storage.resolve_package(package_name, false)?; - let result = installed_guard.use_package(&resolved_package).await; - drop(installed_guard); - match result { - Ok(_) => { - info!( - "{} is linked to binary path", - package_name.color(Color::Blue) - ); - Ok(()) - } - Err(e) => { - if e.to_string() == "NOT_INSTALLED" { - error!("Package is not yet installed."); - let package_name = resolved_package.package.full_name('/'); - self.storage - .install_packages( - &[package_name.to_owned()], - true, - self.installed_packages.clone(), - None, - None, - None, - false, - quiet, - ) - .await?; - - Ok(()) - } else { - Err(e) - } - } - } - } -} - -pub fn select_single_package(packages: &[ResolvedPackage]) -> Result<&ResolvedPackage> { - info!( - "Multiple packages available for {}", - packages[0].package.pkg.clone().color(Color::Blue) - ); - for (i, package) in packages.iter().enumerate() { - info!( - " [{}] [{}] {}: {}", - i + 1, - package.collection.clone().color(Color::BrightGreen), - package.package.full_name('/').color(Color::Blue), - package.package.description - ); - } - - let selection = loop { - let response = interactive_ask( - &format!("Select a package (1-{}): ", packages.len()), - AskType::Normal, - )?; - - match response.parse::() { - Ok(n) if n > 0 && n <= packages.len() => break n - 1, - _ => error!("Invalid selection, please try again."), - } - }; - println!(); - - Ok(&packages[selection]) -} diff --git a/src/registry/storage.rs b/src/registry/storage.rs deleted file mode 100644 index 82ec707..0000000 --- a/src/registry/storage.rs +++ /dev/null @@ -1,556 +0,0 @@ -use std::{ - collections::HashMap, - fs::File, - io::BufReader, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, -}; - -use anyhow::{Context, Result}; -use futures::{future::join_all, StreamExt}; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use serde::{Deserialize, Serialize}; -use tokio::{ - fs, - sync::{Mutex, Semaphore}, -}; -use tracing::{info, warn}; - -use crate::{ - core::{ - color::{Color, ColorExt}, - config::CONFIG, - constant::CACHE_PATH, - file::{get_file_type, FileType}, - util::{build_path, format_bytes, interactive_ask, AskType}, - }, - error, - package::{ - gen_package_info, parse_package_query, run::Runner, Package, PackageQuery, ResolvedPackage, - }, - registry::installed::InstalledPackages, -}; - -use super::select_single_package; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct PackageStorage { - repository: HashMap, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RepositoryPackages { - #[serde(flatten)] - pub collection: HashMap>>, -} - -impl PackageStorage { - pub fn new() -> Self { - Self { - repository: HashMap::new(), - } - } - - pub fn add_repository(&mut self, repo_name: &str, packages: RepositoryPackages) { - self.repository.insert(repo_name.to_owned(), packages); - } - - pub fn resolve_package(&self, package_name: &str, yes: bool) -> Result { - let pkg_query = parse_package_query(package_name); - let mut packages = self - .get_packages(&pkg_query) - .ok_or_else(|| anyhow::anyhow!("Package {} not found", package_name))?; - - packages.sort_by(|a, b| a.package.family.cmp(&b.package.family)); - - let package = if yes || packages.len() == 1 { - &packages[0] - } else { - select_single_package(&packages)? - }; - - Ok(package.to_owned()) - } - - pub async fn install_packages( - &self, - package_names: &[String], - force: bool, - installed_packages: Arc>, - portable: Option, - portable_home: Option, - portable_config: Option, - yes: bool, - quiet: bool, - ) -> Result<()> { - let resolved_packages: Vec = package_names - .iter() - .filter_map(|package_name| { - match self.resolve_package(package_name, yes) { - Ok(package) => Some(package), - Err(err) => { - // Check if a local package is provided instead - let package_path = build_path(package_name).ok()?; - - if package_path.is_file() { - let realpath = if package_path.is_symlink() { - package_path.read_link().ok()? - } else { - package_path - }; - - let file = File::open(&realpath).ok()?; - let mut buf_reader = BufReader::new(&file); - if get_file_type(&mut buf_reader) != FileType::Unknown { - let package_name = realpath.file_name().unwrap().to_string_lossy(); - let size = file.metadata().ok()?.len(); - Some(gen_package_info(&package_name, &realpath, size)) - } else { - error!("{}", err); - None - } - } else { - error!("{}", err); - None - } - } - } - }) - .collect(); - - let results: Vec<_> = join_all(resolved_packages.iter().map(|package| { - let installed_packages = Arc::clone(&installed_packages); - let package = package.clone(); - - async move { - let is_installed = installed_packages.lock().await.is_installed(&package); - (package, is_installed) - } - })) - .await; - - let resolved_packages: Vec = results - .into_iter() - .filter_map(|(package, is_installed)| { - if is_installed { - warn!( - "{} is already installed - {}", - package.package.full_name('/'), - if force { "reinstalling" } else { "skipping" } - ); - - if force { - Some(package) - } else { - None - } - } else { - Some(package) - } - }) - .collect(); - let installed_count = Arc::new(AtomicU64::new(0)); - - let multi_progress = Arc::new(MultiProgress::new()); - let total_progress_bar = if !quiet { - Some(multi_progress.add(ProgressBar::new(resolved_packages.len() as u64))) - } else { - None - }; - - if let Some(pb) = &total_progress_bar { - pb.set_style(ProgressStyle::with_template("Installing {pos}/{len}").unwrap()); - } - - if CONFIG.parallel.unwrap_or_default() { - let semaphore = Arc::new(Semaphore::new(CONFIG.parallel_limit.unwrap_or(2) as usize)); - let mut handles = Vec::new(); - - let pkgs_len = resolved_packages.len(); - for (idx, package) in resolved_packages.iter().enumerate() { - let permit = semaphore.clone().acquire_owned().await.unwrap(); - let package = package.clone(); - let ic = installed_count.clone(); - let installed_packages = installed_packages.clone(); - let portable = portable.clone(); - let portable_home = portable_home.clone(); - let portable_config = portable_config.clone(); - let total_pb = total_progress_bar.clone(); - let multi_progress = if quiet { - None - } else { - Some(multi_progress.clone()) - }; - - let handle = tokio::spawn(async move { - if let Err(e) = package - .install( - idx, - pkgs_len, - installed_packages, - portable, - portable_home, - portable_config, - multi_progress, - ) - .await - { - error!("{}", e); - } else { - ic.fetch_add(1, Ordering::Relaxed); - if let Some(pb) = total_pb { - pb.inc(1); - } - }; - drop(permit); - }); - - handles.push(handle); - } - - for handle in handles { - handle.await?; - } - } else { - for (idx, package) in resolved_packages.iter().enumerate() { - if let Err(e) = package - .install( - idx, - resolved_packages.len(), - installed_packages.clone(), - portable.clone(), - portable_home.clone(), - portable_config.clone(), - if quiet { - None - } else { - Some(multi_progress.clone()) - }, - ) - .await - { - error!("{}", e); - } else { - installed_count.fetch_add(1, Ordering::Relaxed); - if let Some(pb) = &total_progress_bar { - pb.inc(1); - } - }; - } - } - - if let Some(pb) = total_progress_bar { - pb.finish_and_clear(); - } - info!( - "Installed {}/{} packages", - installed_count.load(Ordering::Relaxed).color(Color::Blue), - resolved_packages.len().color(Color::BrightBlue) - ); - Ok(()) - } - - pub async fn remove_packages( - &self, - package_names: &[String], - installed_packages: Arc>, - exact: bool, - ) -> Result<()> { - let mut mut_guard = installed_packages.lock().await; - let installed_packages = &mut_guard.packages; - - let mut packages_to_remove = Vec::new(); - for package_name in package_names.iter() { - let query = parse_package_query(package_name); - let mut matching_packages = Vec::new(); - - for package in installed_packages { - if package.name != query.name { - continue; - } - if let Some(ref ckey) = query.collection { - if package.collection != *ckey { - continue; - } - } - - let family_matches = match (&query.family, &package.family) { - (None, None) => true, - (None, Some(_)) => !exact, - (Some(ref query_family), Some(ref package_family)) => { - query_family == package_family - } - _ => false, - }; - - if family_matches { - matching_packages.push(package.clone()); - } - } - - if matching_packages.is_empty() { - error!("{} is not installed.", package_name); - } else { - packages_to_remove.extend(matching_packages); - } - } - - for package in packages_to_remove { - mut_guard.remove(&package).await?; - } - - Ok(()) - } - - pub fn list_packages(&self, collection: Option<&str>) -> Vec { - let mut packages: Vec = self - .repository - .iter() - .flat_map(|(repo_name, repo_packages)| { - repo_packages - .collection - .iter() - .filter(|(key, _)| collection.is_none() || Some(key.as_str()) == collection) - .flat_map(|(key, collections)| { - collections.iter().flat_map(|(_, packages)| { - packages.iter().map(|package| ResolvedPackage { - repo_name: repo_name.clone(), - collection: key.clone(), - package: package.clone(), - }) - }) - }) - }) - .collect(); - - packages.sort_by(|a, b| { - let collection_cmp = a.collection.cmp(&b.collection); - if collection_cmp == std::cmp::Ordering::Equal { - a.package.full_name('-').cmp(&b.package.full_name('-')) - } else { - collection_cmp - } - }); - packages - } - - pub fn get_packages(&self, query: &PackageQuery) -> Option> { - let pkg_name = query.name.trim(); - let resolved_packages: Vec = self - .repository - .iter() - .flat_map(|(repo_name, packages)| { - packages - .collection - .iter() - .filter(|(collection_key, _)| { - query.collection.is_none() - || Some(collection_key.as_str()) == query.collection.as_deref() - }) - .flat_map(|(collection_key, map)| { - map.get(pkg_name).into_iter().flat_map(|pkgs| { - pkgs.iter().filter_map(|pkg| { - if pkg.pkg.to_lowercase() == pkg_name - && (query.family.is_none() - || pkg.family.as_ref() == query.family.as_ref()) - { - Some(ResolvedPackage { - repo_name: repo_name.to_owned(), - package: pkg.clone(), - collection: collection_key.clone(), - }) - } else { - None - } - }) - }) - }) - }) - .collect(); - - if !resolved_packages.is_empty() { - Some(resolved_packages) - } else { - None - } - } - - pub async fn search(&self, query: &str, case_sensitive: bool) -> Vec { - let query = parse_package_query(query); - let pkg_name = if case_sensitive { - query.name.trim().to_owned() - } else { - query.name.trim().to_lowercase() - }; - let mut resolved_packages: Vec<(u32, Package, String, String)> = Vec::new(); - - for (repo_name, packages) in &self.repository { - for (collection_name, collection_packages) in &packages.collection { - let pkgs: Vec<(u32, Package, String, String)> = collection_packages - .iter() - .flat_map(|(_, packages)| { - packages.iter().filter_map(|pkg| { - let mut score = 0; - let (found_pkg_name, found_pkg_description) = if case_sensitive { - (pkg.pkg.clone(), pkg.description.clone()) - } else { - (pkg.pkg.to_lowercase(), pkg.description.to_lowercase()) - }; - - if found_pkg_name == pkg_name { - score += 5; - } else if found_pkg_name.contains(&pkg_name) { - score += 3; - } else if found_pkg_description.contains(&pkg_name) { - score += 1; - } else { - return None; - } - if query.family.is_none() - || pkg.family.as_ref() == query.family.as_ref() - { - Some(( - score, - pkg.to_owned(), - collection_name.to_owned(), - repo_name.to_owned(), - )) - } else { - None - } - }) - }) - .collect(); - resolved_packages.extend(pkgs); - } - } - - resolved_packages.sort_by(|(a, _, _, _), (b, _, _, _)| b.cmp(a)); - resolved_packages - .into_iter() - .filter(|(score, _, _, _)| *score > 0) - .map(|(_, pkg, collection, repo_name)| ResolvedPackage { - repo_name, - package: pkg, - collection, - }) - .collect() - } - - pub async fn inspect(&self, package_name: &str, inspect_type: &str) -> Result<()> { - let resolved_pkg = self.resolve_package(package_name, false)?; - - let client = reqwest::Client::new(); - let url = if inspect_type == "log" { - resolved_pkg.package.build_log - } else if resolved_pkg - .package - .build_script - .starts_with("https://github.com") - { - resolved_pkg - .package - .build_script - .replacen("/tree/", "/raw/refs/heads/", 1) - .replacen("/blob/", "/raw/refs/heads/", 1) - } else { - resolved_pkg.package.build_script - }; - - let response = client.get(&url).send().await?; - - if !response.status().is_success() { - return Err(anyhow::anyhow!( - "Error fetching build {} from {} [{}]", - inspect_type, - url.color(Color::Blue), - response.status().color(Color::Red) - )); - } - - let content_length = response.content_length().unwrap_or_default(); - if content_length > 1_048_576 { - let response = interactive_ask(&format!( - "The build {} file is too large ({}). Do you really want to download and view it (y/N)? ", - inspect_type, - format_bytes(content_length).color(Color::Magenta) - ), AskType::Warn)?; - - if !response.trim().eq_ignore_ascii_case("y") { - return Err(anyhow::anyhow!("")); - } - } - - info!( - "Fetching {} from {} [{}]", - inspect_type, - url.color(Color::Blue), - format_bytes(response.content_length().unwrap_or_default()).color(Color::Magenta) - ); - - let mut stream = response.bytes_stream(); - - let mut content = Vec::new(); - while let Some(chunk) = stream.next().await { - let chunk = chunk.context("Failed to read chunk")?; - content.extend_from_slice(&chunk); - } - let output = String::from_utf8_lossy(&content).replace("\r", "\n"); - - info!("\n{}", output); - - Ok(()) - } - - pub async fn run(&self, command: &[String], yes: bool) -> Result<()> { - fs::create_dir_all(&*CACHE_PATH).await?; - - let package_name = &command[0]; - let args = if command.len() > 1 { - &command[1..] - } else { - &[] - }; - let runner = if let Ok(resolved_pkg) = self.resolve_package(package_name, yes) { - let package_path = CACHE_PATH.join(&resolved_pkg.package.pkg_name); - Runner::new(&resolved_pkg, package_path, args) - } else { - let query = parse_package_query(package_name); - let package_path = CACHE_PATH.join(&query.name); - let mut resolved_pkg = ResolvedPackage::default(); - resolved_pkg.package.pkg = query.name; - resolved_pkg.package.family = query.family; - - // TODO: check all the repo for package instead of choosing the first - let base_url = CONFIG - .repositories - .iter() - .find_map(|repo| { - if let Some(collection) = &query.collection { - repo.sources.get(collection).cloned() - } else { - repo.sources.values().next().cloned() - } - }) - .ok_or_else(|| anyhow::anyhow!("No repository found for the package"))?; - - resolved_pkg.collection = query.collection.unwrap_or_else(|| { - CONFIG - .repositories - .iter() - .find_map(|repo| repo.sources.keys().next().cloned()) - .unwrap_or_default() - }); - - let download_url = format!("{}/{}", base_url, resolved_pkg.package.full_name('/')); - resolved_pkg.package.download_url = download_url; - Runner::new(&resolved_pkg, package_path, args) - }; - - runner.execute().await?; - - Ok(()) - } -}