diff --git a/.gitignore b/.gitignore index ea8c4bf..f6c313c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/data \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7913500..62dfedf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,18 +17,6 @@ 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 = "aho-corasick" version = "1.1.3" @@ -132,9 +120,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", @@ -207,7 +195,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "based" version = "0.1.0" -source = "git+https://git.hydrar.de/jmarya/based#04852f2fbcc301d0c2b4098f613b9450b4474363" +source = "git+https://git.hydrar.de/jmarya/based#37f65b6353ce1c87dbf71847d02bf787e025a680" dependencies = [ "bcrypt", "chrono", @@ -260,9 +248,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" dependencies = [ "serde", ] @@ -311,11 +299,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] -name = "cc" -version = "1.2.5" +name = "bytesize" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" + +[[package]] +name = "cc" +version = "1.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -532,7 +528,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -651,9 +647,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -680,6 +676,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flume" version = "0.11.1" @@ -697,6 +705,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.3.2" @@ -863,9 +877,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "h2" @@ -891,24 +905,25 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -1267,6 +1282,15 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.76" @@ -1298,22 +1322,32 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.7.0", + "libc", + "redox_syscall", +] + [[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 = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" @@ -1405,12 +1439,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.2" @@ -1467,16 +1495,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1571,7 +1589,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "cfg-if", "foreign-types", "libc", @@ -1620,11 +1638,16 @@ name = "pacco" version = "0.1.0" dependencies = [ "based", + "bytesize", + "chrono", "env_logger 0.11.6", + "log", "maud", "rocket", "serde_json", "sqlx", + "tar", + "zstd", ] [[package]] @@ -1656,12 +1679,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pear" version = "0.2.9" @@ -1702,9 +1719,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1779,9 +1796,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -1864,7 +1881,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", ] [[package]] @@ -2113,11 +2130,11 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "errno", "libc", "linux-raw-sys", @@ -2181,7 +2198,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "core-foundation", "core-foundation-sys", "libc", @@ -2190,9 +2207,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2200,18 +2217,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2220,9 +2237,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", "memchr", @@ -2360,21 +2377,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2385,38 +2392,32 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ - "atoi", - "byteorder", "bytes", "chrono", "crc", "crossbeam-queue", "either", "event-listener", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "hashlink", - "hex", "indexmap", "log", "memchr", "native-tls", "once_cell", - "paste", "percent-encoding", "serde", "serde_json", "sha2", "smallvec", - "sqlformat", "thiserror", "tokio", "tokio-stream", @@ -2427,9 +2428,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" dependencies = [ "proc-macro2", "quote", @@ -2440,9 +2441,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" dependencies = [ "dotenvy", "either", @@ -2466,13 +2467,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.7.0", "byteorder", "bytes", "chrono", @@ -2510,13 +2511,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.7.0", "byteorder", "chrono", "crc", @@ -2524,7 +2525,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -2550,9 +2550,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ "atoi", "chrono", @@ -2616,9 +2616,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.92" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -2664,13 +2664,25 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.14.0" +name = "tar" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2687,18 +2699,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -2773,9 +2785,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -2791,9 +2803,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -3016,12 +3028,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "untrusted" version = "0.7.1" @@ -3059,9 +3065,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" dependencies = [ "getrandom", "serde", @@ -3402,9 +3408,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -3431,6 +3437,17 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xattr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "yansi" version = "1.0.1" @@ -3533,3 +3550,31 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 2f55ecc..a7173ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,13 @@ edition = "2024" [dependencies] based = { git = "https://git.hydrar.de/jmarya/based", features = ["htmx"]} +bytesize = "1.3.0" +chrono = "0.4.39" env_logger = "0.11.6" +log = "0.4.22" maud = "0.26.0" rocket = "0.5.1" serde_json = "1.0.134" sqlx = "0.8.2" +tar = "0.4.43" +zstd = "0.13.2" diff --git a/README.md b/README.md index 56bf630..a288070 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,6 @@ To use the packages pacco provides, add the following to `pacman.conf`: # /etc/pacman.conf [repo] -Include = /etc/pacman.d/mirrorlist_pacco -``` - -Add `/etc/pacman.d/mirrorlist_pacco`: - -``` -# /etc/pacman.d/mirrorlist_pacco - Server = https://example.com/pkg/$repo/$arch ``` diff --git a/src/main.rs b/src/main.rs index 66aa596..7d1ee5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,9 @@ -// TODO : -// - Base -// - API -// - UI -// - PkgDB Abstraction -// - Pkg Abstraction - +use based::auth::User; use based::get_pg; -use based::page::{Shell, render_page}; -use based::request::{RequestContext, StringResponse}; -use maud::html; -use pacco::pkg::Repository; -use rocket::get; use rocket::routes; pub mod routes; -#[get("/")] -pub async fn index_page(ctx: RequestContext) -> StringResponse { - let repos: Vec = Repository::list(); - - let content = html!( - h1 { "Repositories" }; - @for repo in repos { - p { (repo) }; - }; - ); - - render_page( - content, - "Repositories", - ctx, - &Shell::new(html! {}, html! {}, Some(String::new())), - ) - .await -} - #[rocket::launch] async fn launch() -> _ { env_logger::init(); @@ -42,9 +11,21 @@ async fn launch() -> _ { let pg = get_pg!(); sqlx::migrate!("./migrations").run(pg).await.unwrap(); + let _ = User::create("admin".to_string(), "admin", based::auth::UserRole::Admin).await; + rocket::build().mount("/", routes![ - index_page, + based::htmx::htmx_script_route, + routes::index_page, routes::pkg_route, - routes::upload_pkg + routes::push::upload_pkg, + routes::user::login, + routes::user::login_post, + routes::user::account_page, + routes::ui::pkg_ui, + routes::ui::repo_ui, + routes::user::new_api_key, + routes::user::end_session, + routes::user::change_password, + routes::user::change_password_post ]) } diff --git a/src/pkg/package.rs b/src/pkg/package.rs index 5750650..54f3a3c 100644 --- a/src/pkg/package.rs +++ b/src/pkg/package.rs @@ -1,4 +1,10 @@ -use std::path::{Path, PathBuf}; +use std::{ + fs::File, + io::{Cursor, Read}, + path::{Path, PathBuf}, +}; + +use tar::Archive; use super::{Repository, arch::Architecture}; @@ -6,11 +12,11 @@ use super::{Repository, arch::Architecture}; #[derive(Debug, Clone)] pub struct Package { /// Repository of the package - repo: String, + pub repo: String, /// `Architecture` of the package - arch: Architecture, + pub arch: Architecture, /// Name of the package - name: String, + pub name: String, /// Version of the package version: Option, } @@ -20,12 +26,78 @@ impl Package { pub fn new(repo: &str, arch: Architecture, pkg_name: &str, version: &str) -> Self { Package { repo: repo.to_string(), - arch: arch, + arch, name: pkg_name.to_string(), version: Some(version.to_string()), } } + pub fn install_script(&self) -> Option { + let pkg = self.base_path().join(self.file_name()); + read_file_tar(&pkg, ".INSTALL") + } + + pub fn file_list(&self) -> Vec { + list_tar_file(&self.base_path().join(self.file_name())).unwrap() + } + + pub fn binaries(&self) -> Vec { + list_tar_file(&self.base_path().join(self.file_name())) + .unwrap_or_default() + .into_iter() + .filter_map(|x| { + let mut paths: Vec<_> = x.split("/").collect(); + paths.reverse(); + let parent = paths.get(1)?; + if (*parent == "bin" || *parent == "sbin") && !x.ends_with("/") { + return Some(x); + } + + None + }) + .collect() + } + + pub fn pkginfo(&self) -> Vec<(String, String)> { + let content = read_file_tar(&self.base_path().join(self.file_name()), ".PKGINFO").unwrap(); + let mut ret: Vec<(String, Vec)> = Vec::new(); + + for line in content.split("\n") { + if line.starts_with('#') || line.is_empty() { + continue; + } + + let (key, val) = line.split_once(" = ").unwrap(); + + if let Some(e) = ret.iter_mut().find(|x| x.0 == key) { + e.1.push(val.to_string()); + } else { + ret.push((key.to_string(), vec![val.to_string()])); + } + } + + let mut ret: Vec<_> = ret.into_iter().map(|x| (x.0, x.1.join(" "))).collect(); + ret.sort_by(|a, b| a.0.cmp(&b.0)); + + ret + } + + pub fn arch(&self) -> Vec { + let mut ret = Vec::new(); + for a in [ + Architecture::x86_64, + Architecture::aarch64, + Architecture::any, + ] { + let check_pkg = self.switch_arch(a.clone()); + if check_pkg.exists() { + ret.push(a); + } + } + + ret + } + /// Extract values from a package filename /// /// # Example @@ -47,10 +119,12 @@ impl Package { /// ``` pub fn extract_pkg_name(file_name: &str) -> Option<(String, String, String, Architecture)> { // Extract (assuming the filename is "---.pkg.tar.zst") + let file_name = file_name.trim_end_matches(".sig").to_string(); + let mut splitted = file_name.split('-').collect::>(); let arch = splitted.pop()?; - assert!(arch.ends_with(".pkg.tar.zst")); + assert!(arch.ends_with(".pkg.tar.zst"), "{file_name}"); let arch = arch.trim_end_matches(".pkg.tar.zst"); let relation = splitted.pop()?; @@ -58,12 +132,12 @@ impl Package { let pkg_name = splitted.join("-"); - return Some(( + Some(( pkg_name, version.to_string(), relation.to_string(), Architecture::parse(arch)?, - )); + )) } /// Parse a pkg filename @@ -78,25 +152,52 @@ impl Package { } /// Find a package with latest version - pub fn find(repo: &str, arch: &str, pkg_name: &str) -> Self { + pub fn find(repo: &str, arch: Architecture, pkg_name: &str) -> Option { let mut base = Package { repo: repo.to_string(), - arch: Architecture::parse(arch).unwrap(), + arch, name: pkg_name.to_string(), version: None, }; let versions = base.versions(); - let ver = versions.first().unwrap(); + let ver = versions.first()?; base.version = Some(ver.clone()); - base + Some(base) + } + + pub fn systemd_units(&self) -> Vec { + // TODO : Extract unit infos + list_tar_file(&self.base_path().join(self.file_name())) + .unwrap_or_default() + .into_iter() + .filter(|x| { + let ext = x.split(".").last().unwrap(); + ext == "service" || ext == "timer" || ext == "mount" + }) + .collect() + } + + pub fn pacman_hooks(&self) -> Vec<(String, String)> { + let pkg_file = self.base_path().join(self.file_name()); + let files = list_tar_file(&pkg_file).unwrap_or_default(); + files + .into_iter() + .filter(|x| { + x.starts_with("etc/pacman.d/hooks/") || x.starts_with("usr/share/libalpm/hooks/") + }) + .map(|x| { + let content = read_file_tar(&pkg_file, &x).unwrap(); + (x, content) + }) + .collect() } /// Save a new package to repository pub fn save(&self, pkg: Vec, sig: Option>) { - let pkg_file = self.base_path().join(&self.file_name()); + let pkg_file = self.base_path().join(self.file_name()); let sig_file = self.base_path().join(format!("{}.sig", self.file_name())); std::fs::write(&pkg_file, pkg).unwrap(); @@ -106,7 +207,7 @@ impl Package { let db_file = PathBuf::from("./data") .join(&self.repo) - .join(&self.arch.to_string()) + .join(self.arch.to_string()) .join(format!("{}.db.tar.gz", self.repo)); repo_add(db_file.to_str().unwrap(), pkg_file.to_str().unwrap()); @@ -122,7 +223,7 @@ impl Package { let db_file = PathBuf::from("./data") .join(&self.repo) - .join(&arch.to_string()) + .join(arch.to_string()) .join(format!("{}.db.tar.gz", self.repo)); repo_add(db_file.to_str().unwrap(), pkg_file.to_str().unwrap()); @@ -134,16 +235,16 @@ impl Package { // /// let p = Path::new("./data") .join(&self.repo) - .join(&self.arch.to_string()) + .join(self.arch.to_string()) .join(&self.name); std::fs::create_dir_all(&p).unwrap(); p } /// Switch the `Architecture` of the package - pub fn switch_arch(&self, arch: &str) -> Self { + pub fn switch_arch(&self, arch: Architecture) -> Self { let mut new = self.clone(); - new.arch = Architecture::parse(arch).unwrap(); + new.arch = arch; new } @@ -236,3 +337,44 @@ pub fn run_command(cmd: Vec<&str>) { pub fn repo_add(db_file: &str, pkg_file: &str) { run_command(vec!["repo-add", db_file, pkg_file]); } + +pub fn read_file_tar(tar: &PathBuf, file_path: &str) -> Option { + let mut file = File::open(tar).ok()?; + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + let uncompressed = zstd::decode_all(Cursor::new(buf)).unwrap(); + let content = Cursor::new(uncompressed); + let mut a = Archive::new(content); + + for e in a.entries().ok()? { + let mut e = e.ok()?; + let path = e.path().unwrap(); + let path = path.to_str().unwrap(); + if path == file_path { + let mut file_content = Vec::new(); + e.read_to_end(&mut file_content).unwrap(); + return String::from_utf8(file_content).ok(); + } + } + + None +} + +pub fn list_tar_file(tar: &PathBuf) -> Option> { + let mut file = File::open(tar).ok()?; + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + let uncompressed = zstd::decode_all(Cursor::new(buf)).unwrap(); + let content = Cursor::new(uncompressed); + let mut a = Archive::new(content); + let mut paths = Vec::new(); + + for e in a.entries().ok()? { + let e = e.ok()?; + let path = e.path().ok()?; + let path = path.to_str()?; + paths.push(path.to_string()); + } + + Some(paths) +} diff --git a/src/pkg/repo.rs b/src/pkg/repo.rs index 35affb0..4a48698 100644 --- a/src/pkg/repo.rs +++ b/src/pkg/repo.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{collections::HashSet, path::PathBuf}; use super::{Package, arch::Architecture}; @@ -68,7 +68,7 @@ impl Repository { pub fn base_path(&self, arch: Architecture) -> PathBuf { PathBuf::from("./data") .join(&self.name) - .join(&arch.to_string()) + .join(arch.to_string()) } /// Get the `.db.tar.gz` content for the repository of `arch` @@ -89,23 +89,42 @@ impl Repository { .ok() } - pub fn get_pkg(&self, pkg_name: &str) -> Option { - // Normalize name - let pkg_name = if pkg_name.ends_with(".sig") { - pkg_name.trim_end_matches(".sig").to_string() - } else { - pkg_name.to_string() - }; + pub fn list_pkg(&self) -> Vec { + let mut packages = HashSet::new(); + for arch in self.arch() { + for entry in std::fs::read_dir(self.base_path(arch)).unwrap().flatten() { + let path = entry.path(); + let file_name = path.file_name().unwrap().to_str().unwrap().to_string(); + + if entry.metadata().unwrap().is_dir() { + packages.insert(file_name); + } + } + } + + let mut pkg: Vec<_> = packages.into_iter().collect(); + pkg.sort(); + + pkg + } + + pub fn get_pkg_by_name(&self, pkg_name: &str) -> Option { + for arch in self.arch() { + if let Some(pkg) = Package::find(&self.name, arch, pkg_name) { + return Some(pkg); + } + } + + None + } + + pub fn get_pkg(&self, pkg_name: &str) -> Option { // Find package - let (name, version, _, arch) = Package::extract_pkg_name(&pkg_name).unwrap(); + let (name, version, _, arch) = Package::extract_pkg_name(pkg_name).unwrap(); let pkg = Package::new(&self.name, arch, &name, &version); // Return if exists - if pkg.exists() { - return Some(pkg); - } else { - None - } + if pkg.exists() { Some(pkg) } else { None } } } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 14cfbac..3a454a9 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,71 +1,44 @@ -use based::request::api::{FallibleApiResponse, api_error}; -use based::request::{RawResponse, RequestContext, respond_with}; +use based::auth::MaybeUser; +use based::page::{Shell, htmx_link, render_page}; +use based::request::{RawResponse, RequestContext, StringResponse, respond_with}; +use maud::{PreEscaped, html}; +use rocket::get; use rocket::http::{ContentType, Status}; -use rocket::tokio::io::AsyncReadExt; -use rocket::{FromForm, get, post}; -use serde_json::json; +use pacco::pkg::Repository; use pacco::pkg::arch::Architecture; -use pacco::pkg::{Package, Repository}; -// /pkg/// -// /pkg/// +pub mod push; +pub mod ui; +pub mod user; -use rocket::form::Form; -use rocket::fs::TempFile; +#[get("/")] +pub async fn index_page(ctx: RequestContext, user: MaybeUser) -> StringResponse { + let repos: Vec = Repository::list(); -#[derive(FromForm)] -pub struct PkgUpload<'r> { - name: String, - arch: String, - version: String, - pkg: TempFile<'r>, - sig: Option>, -} - -pub async fn tmp_file_to_vec<'r>(tmp: &TempFile<'r>) -> Vec { - let mut buf = Vec::with_capacity(tmp.len() as usize); - tmp.open() - .await - .unwrap() - .read_to_end(&mut buf) - .await - .unwrap(); - buf -} - -#[post("/pkg//upload", data = "")] -pub async fn upload_pkg( - repo: &str, - upload: Form>, - user: based::auth::APIUser, -) -> FallibleApiResponse { - // TODO : Permission System - if !user.0.is_admin() { - return Err(api_error("Forbidden")); - } - - let pkg = Package::new( - repo, - Architecture::parse(&upload.arch).ok_or_else(|| api_error("Invalid architecture"))?, - &upload.name, - &upload.version, + let content = html!( + div class="flex justify-between" { + h1 class="text-4xl font-bold pb-6" { "Repositories" }; + @if let Some(user) = user.take_user() { + a class="text-lg" href="/account" { (user.username) }; + }; + }; + div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" { + @for repo in repos { + (htmx_link(&format!("/{repo}"), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! { + p class="font-medium flex-1 text-gray-800 dark:text-gray-100" { + (repo) + }; + })) + } + }; ); - pkg.save( - tmp_file_to_vec(&upload.pkg).await, - if let Some(sig) = &upload.sig { - Some(tmp_file_to_vec(sig).await) - } else { - None - }, - ); - - Ok(json!({"ok": format!("Added '{}' to '{}'", pkg.file_name(), repo)})) + render(content, "Repositories", ctx).await } #[get("/pkg///")] -pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str, ctx: RequestContext) -> RawResponse { +pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str) -> RawResponse { let arch = Architecture::parse(arch).unwrap(); if let Some(repo) = Repository::new(repo) { @@ -112,3 +85,25 @@ pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str, ctx: RequestConte "Not found".as_bytes().to_vec(), ) } + +pub async fn render( + content: PreEscaped, + title: &str, + ctx: RequestContext, +) -> StringResponse { + render_page( + content, + title, + ctx, + &Shell::new( + html! { + script src="https://cdn.tailwindcss.com" {}; + script src="/assets/htmx.min.js" {}; + meta name="viewport" content="width=device-width, initial-scale=1.0"; + }, + html! {}, + Some("bg-gray-900 text-gray-200 min-h-screen p-10".to_string()), + ), + ) + .await +} diff --git a/src/routes/push.rs b/src/routes/push.rs new file mode 100644 index 0000000..5d51c57 --- /dev/null +++ b/src/routes/push.rs @@ -0,0 +1,63 @@ +use based::request::api::{FallibleApiResponse, api_error}; +use rocket::tokio::io::AsyncReadExt; +use rocket::{FromForm, post}; +use serde_json::json; + +use pacco::pkg::Package; +use pacco::pkg::arch::Architecture; + +// /pkg/// +// /pkg/// + +use rocket::form::Form; +use rocket::fs::TempFile; + +#[derive(FromForm)] +pub struct PkgUpload<'r> { + name: String, + arch: String, + version: String, + pkg: TempFile<'r>, + sig: Option>, +} + +pub async fn tmp_file_to_vec<'r>(tmp: &TempFile<'r>) -> Vec { + let mut buf = Vec::with_capacity(tmp.len() as usize); + tmp.open() + .await + .unwrap() + .read_to_end(&mut buf) + .await + .unwrap(); + buf +} + +#[post("/pkg//upload", data = "")] +pub async fn upload_pkg( + repo: &str, + upload: Form>, + user: based::auth::APIUser, +) -> FallibleApiResponse { + // TODO : Permission System + if !user.0.is_admin() { + return Err(api_error("Forbidden")); + } + + let pkg = Package::new( + repo, + Architecture::parse(&upload.arch).ok_or_else(|| api_error("Invalid architecture"))?, + &upload.name, + &upload.version, + ); + + pkg.save( + tmp_file_to_vec(&upload.pkg).await, + if let Some(sig) = &upload.sig { + Some(tmp_file_to_vec(sig).await) + } else { + None + }, + ); + + Ok(json!({"ok": format!("Added '{}' to '{}'", pkg.file_name(), repo)})) +} diff --git a/src/routes/ui.rs b/src/routes/ui.rs new file mode 100644 index 0000000..4cd0c8a --- /dev/null +++ b/src/routes/ui.rs @@ -0,0 +1,292 @@ +use based::{ + page::htmx_link, + request::{RequestContext, StringResponse}, +}; +use maud::{PreEscaped, html}; +use rocket::get; + +use pacco::pkg::Repository; + +use super::render; + +// TODO : API + +#[get("//")] +pub async fn pkg_ui(repo: &str, pkg_name: &str, ctx: RequestContext) -> Option { + // TODO : Implement pkg UI + // pkgmeta display + + let repo = Repository::new(repo).unwrap(); + let pkg = repo.get_pkg_by_name(pkg_name)?; + let versions = pkg.versions(); + let arch = pkg.arch(); + let install_script = pkg.install_script(); + let systemd_units = pkg.systemd_units(); + let pacman_hooks = pkg.pacman_hooks(); + let binaries = pkg.binaries(); + let mut pkginfo = pkg.pkginfo(); + + let content = html! { + // Package Name + div class="flex flex-wrap gap-2 justify-center items-center" { + (htmx_link(&format!("/{}", repo.name), "text-3xl font-bold text-gray-800 dark:text-gray-100", "", html! { + (repo.name) + })) + p class="font-bold p-2 text-gray-400" { "/" }; + h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 pr-2" { + (pkg.name) + }; + + @if pkg.is_signed() { + div class="flex items-center gap-2 text-slate-300 pr-4" { + span class="text-2xl font-bold" { + "✓" + } + span class="text-sm font-medium" { "Signed" } + // TODO : Add more info: Who signed? + Public Key recv + } + } + + @for arch in arch { + span class="px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full" { + (arch.to_string()) + }; + } + + a href=(format!("/pkg/{}/{}/{}", pkg.repo, pkg.arch.to_string(), pkg.file_name())) class="ml-4 inline-flex items-center px-2 py-2 bg-gray-200 text-black font-xs rounded-lg shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" { + svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" { + path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m7-7l-7 7-7-7" {}; + }; + p class="ml-2" { "Download" }; + }; + + }; + + div class="flex pt-6" { + div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition flex-1 mr-4" { + h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 underline" { "Info" }; + + @if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "pkgdesc" }).1 { + (build_info(desc.0, desc.1)) + } + + @if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "packager" }).1 { + (build_info(desc.0, desc.1)) + } + + @if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "url" }).1 { + (build_info(desc.0, desc.1)) + } + + @if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "size" }).1 { + (build_info(desc.0, desc.1)) + } + + @for (key, val) in pkginfo { + (build_info(key, val)) + }; + }; + + div class="space-y-2 max-w-80 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition" { + h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { "Versions" } + ul class="space-y-1" { + @for version in versions { + // TODO : Implement page per version ?version= + li class="text-gray-800 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400 transition" { + (version) + }; + } + } + } + }; + + div class="flex flex-wrap pt-6" { + div class="space-y-2" { + h2 class="text-xl font-bold text-gray-700 dark:text-gray-300" { "Content" } + + div class="flex" { + @if !systemd_units.is_empty() { + div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition mr-4" { + h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Systemd Units" } + ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { + @for unit in systemd_units { + li { (unit) } + } + } + } + } + + @if !pacman_hooks.is_empty() { + div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition mr-4" { + h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Pacman Hooks" } + ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { + @for hook in pacman_hooks { + h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { (hook.0) } + + pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg text-gray-800 dark:text-gray-100 overflow-x-auto text-sm" { + (hook.1) + } + } + } + } + } + + @if !binaries.is_empty() { + div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition mr-4" { + h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Binaries" } + ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { + @for binary in binaries { + li { (binary) } + } + } + } + } + + div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition mr-4" { + h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Package Files" } + ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { + @for file in pkg.file_list() { + li { (file) } + } + } + } + } + }; + }; + + // Install Scripts + @if let Some(install_script) = install_script { + div class="space-y-4 pt-6" { + h2 class="text-3xl font-semibold text-gray-700 dark:text-gray-300" { "Install Scripts" } + pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg text-gray-800 dark:text-gray-100 overflow-x-auto text-sm" { + (install_script) + } + }; + } + }; + + Some(render(content, pkg_name, ctx).await) +} + +#[get("/")] +pub async fn repo_ui(repo: &str, ctx: RequestContext) -> StringResponse { + // TODO : Repo UI + // permissions + // pkg list + + let repo = Repository::new(repo).unwrap(); + let architectures: Vec<_> = repo.arch().into_iter().map(|x| x.to_string()).collect(); + let packages = repo.list_pkg(); + + let content = html! { + // Repository name and architectures + div class="flex flex-wrap items-center justify-center mb-6" { + h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 pr-4" { + (repo.name) + }; + + div class="flex gap-2 mt-2 md:mt-0" { + @for arch in architectures { + span class="px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full" { + (arch) + }; + } + } + }; + + // Package list + ul class="space-y-4" { + @for pkg in packages { + (htmx_link(&format!("/{}/{pkg}", repo.name), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! { + div class="w-10 h-10 flex items-center justify-center rounded-full bg-blue-500 text-white font-semibold" { + {(pkg.chars().next().unwrap_or_default().to_uppercase())} + }; + p class="font-medium flex-1 text-gray-800 dark:text-gray-100" { + (pkg) + }; + })) + } + } + }; + + render(content, &repo.name, ctx).await +} + +pub fn build_info(key: String, value: String) -> PreEscaped { + match key.as_str() { + "pkgname" => {} + "xdata" => {} + "arch" => {} + "pkbase" => {} + "pkgver" => {} + "pkgdesc" => { + return key_value("Description".to_string(), value); + } + "packager" => { + return key_value("Packager".to_string(), value); + } + "url" => { + return html! { + div class="flex" { + span class="font-bold w-32" { (format!("Website: ")) }; + a class="ml-2 text-blue-400" href=(value) { (value) }; + }; + }; + } + "builddate" => { + let date = chrono::DateTime::from_timestamp(value.parse().unwrap(), 0).unwrap(); + return key_value( + "Build at".to_string(), + date.format("%d.%m.%Y %H:%M").to_string(), + ); + } + "depend" => { + // TODO : Find package link + return key_value("Depends on".to_string(), value); + } + "makedepend" => { + // TODO : Find package link + return key_value("Build Dependencies".to_string(), value); + } + "license" => { + return key_value("License".to_string(), value); + } + "size" => { + return key_value( + "Size".to_string(), + bytesize::to_string(value.parse().unwrap(), false), + ); + } + _ => { + log::warn!("Unhandled PKGINFO {key} = {value}"); + } + } + + html! {} +} + +pub fn key_value(key: String, value: String) -> PreEscaped { + html! { + div class="flex" { + span class="font-bold w-32" { (format!("{key}: ")) }; + span class="ml-2" { (value) }; + }; + } +} + +pub fn take_out(v: &mut Vec, f: impl Fn(&T) -> bool) -> (&mut Vec, Option) { + let mut index = -1; + + for (i, e) in v.iter().enumerate() { + if f(e) { + index = i as i64; + } + } + + if index != -1 { + let e = v.remove(index as usize); + return (v, Some(e)); + } + + (v, None) +} diff --git a/src/routes/user.rs b/src/routes/user.rs new file mode 100644 index 0000000..d8975b3 --- /dev/null +++ b/src/routes/user.rs @@ -0,0 +1,222 @@ +use based::{ + auth::{Session, Sessions, User, csrf::CSRF}, + page::htmx_link, + request::{RequestContext, StringResponse, api::to_uuid, respond_html}, +}; +use maud::{PreEscaped, html}; +use rocket::{ + FromForm, + form::Form, + get, + http::{Cookie, CookieJar}, + post, + response::Redirect, +}; + +use super::render; + +#[get("/login")] +pub async fn login(ctx: RequestContext) -> StringResponse { + let content = html! { + div class="min-w-screen justify-center flex items-center" { + div class="bg-gray-800 shadow-lg rounded-lg p-8 max-w-sm w-full" { + h2 class="text-2xl font-bold text-center text-gray-100 mb-6" { "Login" }; + form action="/login" method="POST" { + div class="mb-4" { + label for="email" class="block text-sm font-medium text-gray-400" { "Email" }; + input type="text" id="username" name="username" placeholder="Username" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {}; + }; + div class="mb-4" { + label for="password" class="block text-sm font-medium text-gray-400" { "Password" }; + input type="password" id="password" name="password" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {}; + }; + button type="submit" class="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400" { "Login" }; + }; + }; + }; + }; + + render(content, "Login", ctx).await +} + +#[derive(FromForm)] +pub struct LoginForm { + username: String, + password: String, +} + +#[post("/login", data = "
")] +pub async fn login_post(form: Form, cookies: &CookieJar<'_>) -> Redirect { + if let Some(user) = User::login(&form.username, &form.password).await { + let session_cookie = Cookie::build(("session", user.0.token.to_string())) + .path("/") + .http_only(true) + .max_age(rocket::time::Duration::days(7)) + .build(); + + cookies.add(session_cookie); + + Redirect::to("/") + } else { + Redirect::to("/login") + } +} + +#[get("/end_session/?")] +pub async fn end_session(session_id: &str, csrf: &str, user: User) -> StringResponse { + if user.verify_csrf(csrf).await { + user.end_session(&to_uuid(session_id).unwrap()).await; + respond_html( + html! { + (user.update_csrf().await) + } + .into_string(), + ) + } else { + respond_html(html! { p { "Invalid CSRF" }}.into_string()) + } +} + +#[get("/new_api_key?&")] +pub async fn new_api_key(user: User, csrf: &str, session_name: &str) -> StringResponse { + if user.verify_csrf(csrf).await { + if session_name.is_empty() { + return respond_html( + html! { + div id="next_session" {}; + (user.update_csrf().await) + } + .into_string(), + ); + } + + let api = user.api_key(session_name).await; + respond_html( + html! { + li class="justify-between items-center bg-gray-50 p-4 rounded shadow" { + span class="text-gray-700" { (api.name.unwrap_or_default()) }; + br {}; + span class="text-red-500" { (api.token) }; + }; + + div id="next_session" {}; + (user.update_csrf().await) + } + .into_string(), + ) + } else { + respond_html( + html! { + p { "CSRF!" }; + } + .into_string(), + ) + } +} + +#[get("/account")] +pub async fn account_page(user: User, ctx: RequestContext) -> StringResponse { + let sessions = user.list_sessions().await; + + let content = html! { + main class="w-full mt-6 rounded-lg shadow-md p-6" { + section class="mb-6" { + h2 class="text-xl font-semibold mb-2" { (user.username) }; + }; + + (htmx_link("/passwd", "mb-6 bg-green-500 text-white py-2 px-6 rounded hover:bg-green-600", "", html! { "Change Password" })) + + section class="mb-6 mt-6" { + h3 class="text-lg font-semibold mb-4" { "Active Sessions" }; + ul class="space-y-4" id="sessions-list" { + + @for ses in sessions { + (build_session_block(&ses, &user).await) + }; + + div id="next_session" {}; + + } + } + + form class="flex" hx-get="/new_api_key" hx-target="#next_session" + hx-swap="outerHTML" { + input type="text" name="session_name" placeholder="API Key Name" class="text-black bg-gray-100 px-4 py-2 rounded mr-4" {}; + input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {}; + button class="bg-green-500 text-white py-2 px-6 rounded hover:bg-green-600" { "New API Key" }; + } + }; + }; + + render(content, "Account", ctx).await +} + +pub async fn build_session_block(ses: &Session, user: &User) -> PreEscaped { + html! { + li class="flex justify-between items-center bg-gray-50 p-4 rounded shadow" { + span class="text-gray-700" { (ses.name.clone().unwrap_or("Session".to_string())) }; + + form hx-target="closest li" hx-swap="outerHTML" hx-get=(format!("/end_session/{}", ses.id)) { + input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {}; + button class="bg-red-500 text-white py-1 px-4 rounded hover:bg-red-600" + { "Kill" }; + }; + } + } +} + +#[derive(FromForm)] +pub struct PasswordChangeForm { + password_old: String, + password_new: String, + password_new_repeat: String, + csrf: String, +} + +// TODO : Change password pages + +#[post("/passwd", data = "")] +pub async fn change_password_post(form: Form, user: User) -> Redirect { + if form.password_new != form.password_new_repeat { + return Redirect::to("/passwd"); + } + + if !user.verify_csrf(&form.csrf).await { + return Redirect::to("/passwd"); + } + + user.passwd(&form.password_old, &form.password_new) + .await + .unwrap(); + + Redirect::to("/account") +} + +#[get("/passwd")] +pub async fn change_password(ctx: RequestContext, user: User) -> StringResponse { + let content = html! { + div class="min-w-screen justify-center flex items-center" { + div class="bg-gray-800 shadow-lg rounded-lg p-8 max-w-sm w-full" { + h2 class="text-2xl font-bold text-center text-gray-100 mb-6" { "Change Password" }; + form action="/passwd" method="POST" { + div class="mb-4" { + label for="password_old" class="block text-sm font-medium text-gray-400" { "Old Password" }; + input type="password" id="password_old" name="password_old" placeholder="Old Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {}; + }; + div class="mb-4" { + label for="password_new" class="block text-sm font-medium text-gray-400" { "New Password" }; + input type="password" id="password_new" name="password_new" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {}; + }; + div class="mb-4" { + label for="password_new_repeat" class="block text-sm font-medium text-gray-400" { "Repeat new Password" }; + input type="password" id="password_new_repeat" name="password_new_repeat" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {}; + }; + input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {}; + button type="submit" class="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400" { "Login" }; + }; + }; + }; + }; + + render(content, "Change Password", ctx).await +}