diff --git a/Cargo.lock b/Cargo.lock index dec2400..8fe911d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,6 +658,26 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -958,6 +978,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fax" version = "0.2.6" @@ -1010,6 +1036,27 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1122,11 +1169,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -1209,6 +1269,15 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1335,6 +1404,22 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1353,9 +1438,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1463,6 +1550,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "0.3.0" @@ -1651,6 +1744,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -1710,6 +1809,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -1955,6 +2060,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2096,6 +2218,50 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "owo-colors" version = "4.2.3" @@ -2203,6 +2369,19 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "pile-client" +version = "0.0.2" +source = "git+https://git.betalupi.com/Mark/pile.git?rev=90c5584513acde6f30f76d70c426cf6987643c1a#90c5584513acde6f30f76d70c426cf6987643c1a" +dependencies = [ + "axum", + "bytes", + "futures-core", + "reqwest", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2291,6 +2470,16 @@ dependencies = [ "prettytable-rs", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "prettytable-rs" version = "0.10.0" @@ -2502,6 +2691,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -2717,10 +2912,12 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -2731,6 +2928,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -2813,6 +3011,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.37" @@ -2869,6 +3080,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2881,6 +3101,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -3002,6 +3245,26 @@ dependencies = [ "tower-http", ] +[[package]] +name = "service-pile" +version = "0.0.1" +dependencies = [ + "axum", + "grass", + "libservice", + "maud", + "mime", + "pile-client", + "reqwest", + "serde", + "servable", + "service-assets", + "tokio", + "tower-http", + "tracing", + "url", +] + [[package]] name = "service-webpage" version = "0.0.1" @@ -3323,6 +3586,40 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "term" version = "0.7.0" @@ -3537,6 +3834,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -3958,6 +4265,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[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" @@ -3998,6 +4311,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.106" @@ -4056,6 +4378,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -4069,6 +4413,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -4099,6 +4455,7 @@ dependencies = [ "libservice", "serde", "service-assets", + "service-pile", "service-webpage", "tokio", "toolbox", @@ -4192,6 +4549,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -4318,6 +4686,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" diff --git a/Cargo.toml b/Cargo.toml index f8730a2..7868149 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,8 +70,11 @@ md-footnote = { path = "crates/lib/md-footnote" } md-dev = { path = "crates/lib/md-dev" } service-webpage = { path = "crates/service/service-webpage" } +service-pile = { path = "crates/service/service-pile" } service-assets = { path = "crates/service/service-assets" } +pile-client = { git = "https://git.betalupi.com/Mark/pile.git", rev = "90c5584513acde6f30f76d70c426cf6987643c1a" } + # # MARK: Server diff --git a/crates/bin/webpage/Cargo.toml b/crates/bin/webpage/Cargo.toml index 4b8a5c0..6e80034 100644 --- a/crates/bin/webpage/Cargo.toml +++ b/crates/bin/webpage/Cargo.toml @@ -13,6 +13,7 @@ libservice = { workspace = true } service-webpage = { workspace = true } service-assets = { workspace = true } +service-pile = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } diff --git a/crates/bin/webpage/src/cmd/serve.rs b/crates/bin/webpage/src/cmd/serve.rs index 14c3063..9a712d7 100644 --- a/crates/bin/webpage/src/cmd/serve.rs +++ b/crates/bin/webpage/src/cmd/serve.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use libservice::{Service, ServiceConnectInfo, ToService}; use service_assets::AssetService; +use service_pile::PileService; use service_webpage::WebpageService; use std::sync::Arc; use tracing::{error, info}; @@ -72,10 +73,15 @@ pub struct RouterState {} pub async fn make_service(_state: Option>) -> Result { let service_webpage = WebpageService::new(); let service_assets = AssetService::new(); + let service_pile = PileService::new() + .await + .map_err(|e| anyhow::anyhow!(e)) + .context("while initializing pile datasets")?; Ok(Service::new() .merge(service_webpage) .nest("/assets", service_assets) + .nest("/pile", service_pile) .to_service() .trace()) } diff --git a/crates/lib/toolbox/src/logging.rs b/crates/lib/toolbox/src/logging.rs index 5613a57..8133356 100644 --- a/crates/lib/toolbox/src/logging.rs +++ b/crates/lib/toolbox/src/logging.rs @@ -42,6 +42,7 @@ pub struct LoggingConfig { // Libs pub libservice: LogLevel, + pub servable: LogLevel, pub toolbox: LogLevel, // Bins @@ -67,15 +68,22 @@ impl From for EnvFilter { format!("axum={}", conf.silence), format!("selectors={}", conf.silence), format!("html5ever={}", conf.silence), + format!("tantivy={}", conf.silence), + format!("aws_smithy_runtime={}", conf.silence), + format!("aws_smithy_http_client={}", conf.silence), + format!("aws_sdk_s3={}", conf.silence), + format!("aws_sigv4={}", conf.silence), // // Libs // format!("toolbox={}", conf.toolbox), format!("libservice={}", conf.libservice), + format!("servable={}", conf.servable), // // Bins // format!("service_webpage={}", conf.service), + format!("service_pile={}", conf.service), format!("webpage={}", conf.webpage), conf.other.to_string(), ] @@ -183,6 +191,7 @@ impl LogFilterPreset { // Libs libservice: LogLevel::Error, + servable: LogLevel::Error, toolbox: LogLevel::Error, // Bins @@ -196,6 +205,7 @@ impl LogFilterPreset { // Libs libservice: LogLevel::Warn, + servable: LogLevel::Warn, toolbox: LogLevel::Warn, // Bins @@ -209,6 +219,7 @@ impl LogFilterPreset { // Libs libservice: LogLevel::Info, + servable: LogLevel::Info, toolbox: LogLevel::Info, // Bins @@ -222,6 +233,7 @@ impl LogFilterPreset { // Libs libservice: LogLevel::Debug, + servable: LogLevel::Debug, toolbox: LogLevel::Debug, // Bins @@ -235,6 +247,7 @@ impl LogFilterPreset { // Libs libservice: LogLevel::Trace, + servable: LogLevel::Trace, toolbox: LogLevel::Trace, // Bins @@ -248,6 +261,7 @@ impl LogFilterPreset { // Libs libservice: LogLevel::Trace, + servable: LogLevel::Trace, toolbox: LogLevel::Trace, // Bins @@ -261,6 +275,7 @@ impl LogFilterPreset { // Libs libservice: LogLevel::Trace, + servable: LogLevel::Trace, toolbox: LogLevel::Trace, // Bins diff --git a/crates/service/service-pile/Cargo.toml b/crates/service/service-pile/Cargo.toml new file mode 100644 index 0000000..b0de2f5 --- /dev/null +++ b/crates/service/service-pile/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "service-pile" +version = { workspace = true } +rust-version = { workspace = true } +edition = { workspace = true } + +[lints] +workspace = true + +[dependencies] +libservice = { workspace = true } +service-assets = { workspace = true } + +pile-client = { workspace = true } + +tracing = { workspace = true } +grass = { workspace = true } +axum = { workspace = true } +maud = { workspace = true } +serde = { workspace = true } +reqwest = { workspace = true } +tower-http = { workspace = true } +servable = { workspace = true } +url = { workspace = true } +mime = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true } diff --git a/crates/service/service-pile/css/main.scss b/crates/service/service-pile/css/main.scss new file mode 100644 index 0000000..a19b338 --- /dev/null +++ b/crates/service/service-pile/css/main.scss @@ -0,0 +1,183 @@ +@import "text"; + +:root { + // Misc colors + --bgColor: #121212; + --lightBgColor: #3a3f46; + --fgColor: #ebebeb; + --metaColor: #6199bb; + --lightMetaColor: #638c86; + --linkColor: #e4dab3; + --codeBgColor: #292929; + --codeFgColor: var(--fgColor); + + // Main colors + --grey: #696969; + + // Accent colors, used only manally + --green: #a2c579; + --magenta: #ad79c5; + --orange: #e86a33; + --yellow: #e8bc00; + --pink: #fa9f83; +} + +::selection, +::-moz-selection { + color: var(--bgColor); + background: var(--metaColor); +} + +html { + -webkit-box-sizing: border-box; + box-sizing: border-box; + font-size: 62.5%; + scrollbar-color: var(--metaColor) var(--bgColor); + scrollbar-width: auto; + background: var(--bgColor); +} + +body { + font-family: "Fira"; + font-size: 1.6rem; + line-height: 1.35; + max-width: 64rem; + margin: auto; + overflow-wrap: break-word; + background: var(--bgColor); + color: var(--fgColor); +} + +div.wrapper { + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: space-between; +} + + +@media (max-width: 650px) { + .wrapper { + margin: 1rem; + } +} + +// Loading spinner (three dots) +.htmx-indicator { + display: none; +} +.htmx-request .htmx-indicator, +.htmx-request.htmx-indicator { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 8px 0; +} + +@keyframes dot-bounce { + 0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); } + 40% { opacity: 1; transform: scale(1); } +} + +.search-meta { + font-size: 1.2rem; + color: var(--grey); + margin: 0 0 1.5em 0; + text-align: left; +} + +#search-results { + margin-top: 0; +} + +.result-grid { + display: flex; + flex-direction: column; + gap: 0.5em; + margin-top: 1.5em; +} + +.result-item { + display: flex; + align-items: stretch; + border: 1px solid var(--lightBgColor); + border-radius: 3px; + overflow: hidden; + height: 64px; +} + +.result-item-thumb { + width: 64px; + flex-shrink: 0; + background: var(--lightBgColor); + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } +} + +.result-item-info { + padding: 0.5em 0.8em; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.15em; + min-width: 0; + flex: 1; +} + +.result-item-key { + font-family: monospace; + font-size: 1.3rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; + text-align: left; + color: var(--fgColor); + cursor: pointer; + + &:hover { + color: var(--metaColor); + } +} + +.result-item-link { + font-family: monospace; + font-size: 1.1rem; + color: var(--linkColor); + text-decoration: none; + align-self: flex-start; + + &:hover { + text-decoration: underline; + } +} + +.result-sentinel { + height: 1px; +} + +#preview-overlay { + display: none; + position: fixed; + z-index: 1000; + pointer-events: none; + border: 1px solid var(--lightBgColor); + background: var(--bgColor); + padding: 4px; + border-radius: 3px; + box-shadow: 0 4px 24px rgba(0,0,0,0.6); + + img { + display: block; + max-width: 480px; + max-height: 480px; + object-fit: contain; + } +} diff --git a/crates/service/service-pile/css/text.scss b/crates/service/service-pile/css/text.scss new file mode 100644 index 0000000..e977231 --- /dev/null +++ b/crates/service/service-pile/css/text.scss @@ -0,0 +1,29 @@ +h1 { + font-size: 3.5rem; + margin-top: 1ex; + margin-bottom: 1ex; +} + +h2 { + font-size: 2.5rem; + margin-top: 1ex; + margin-bottom: 0.5ex; +} + +h3 { + font-size: 2rem; +} + +a { + text-decoration: none; + border-radius: .3rem; + padding: 0 .2ex 0 .2ex; + color: var(--linkColor); + transition: 150ms; +} + +a:hover { + background-color: var(--linkColor); + color: var(--bgColor); + transition: 150ms; +} diff --git a/crates/service/service-pile/src/lib.rs b/crates/service/service-pile/src/lib.rs new file mode 100644 index 0000000..d16cb9d --- /dev/null +++ b/crates/service/service-pile/src/lib.rs @@ -0,0 +1,37 @@ +use axum::Router; +use libservice::ToService; +use pile_client::PileClient; +use std::sync::Arc; + +mod pages; +mod routes; + +pub const PILE_PREFIX: &str = "/pile"; +pub const ASSET_PREFIX: &str = "/assets"; + +pub struct PileService { + client: Arc, +} + +impl PileService { + pub async fn new() -> Result> { + let endpoint = std::env::var("PILE_ENDPOINT")?; + let api_key = std::env::var("PILE_API_KEY").ok(); + let client = PileClient::new(&endpoint, api_key.as_deref())?; + Ok(Self { + client: Arc::new(client), + }) + } +} + +impl ToService for PileService { + #[inline] + fn make_router(&self) -> Option> { + Some(routes::router(self.client.clone())) + } + + #[inline] + fn service_name(&self) -> Option { + Some("pile".to_owned()) + } +} diff --git a/crates/service/service-pile/src/pages/index.rs b/crates/service/service-pile/src/pages/index.rs new file mode 100644 index 0000000..fe438db --- /dev/null +++ b/crates/service/service-pile/src/pages/index.rs @@ -0,0 +1,110 @@ +use maud::{Markup, html}; +use servable::{HtmlPage, PageMetadata, RenderContext}; +use service_assets::assets::{CSS_FIRA, CSS_FONTAWESOME, HTMX}; +use std::{pin::Pin, sync::LazyLock}; + +use crate::{ASSET_PREFIX, PILE_PREFIX, routes::CSS_PILE}; + +pub static INDEX: LazyLock = LazyLock::new(|| { + HtmlPage::default() + .with_style_linked(CSS_PILE.route_at(PILE_PREFIX)) + .with_style_linked(CSS_FIRA.route_at(ASSET_PREFIX)) + .with_style_linked(CSS_FONTAWESOME.route_at(ASSET_PREFIX)) + .with_script_linked(HTMX.route_at(ASSET_PREFIX)) + .with_meta(PageMetadata { + title: "Pile".into(), + author: None, + description: None, + image: None, + }) + .with_render(render) +}); + +fn render<'a>( + _page: &'a HtmlPage, + _ctx: &'a RenderContext, +) -> Pin + Send + Sync + 'a>> { + Box::pin(async { + html! { + div class="wrapper" style="margin-top:3ex;" { + div { + div style=" + text-align:center; + padding-top:30px; + padding-bottom:60px; + " { + h1 class="brand" { + span class="fa fa-solid fa-book" aria-hidden="true" {} + " Library search" + } + + div style="max-width:500px;margin:0 auto;padding:.4em 1em;" { + form { + input + class="search-input" + id="search" + name="q" + type="text" + placeholder="Type to search..." + style=" + -moz-box-sizing: border-box !important; + box-sizing: border-box !important; + outline: none; + border: none; + border-radius: 1px; + margin-top: 5px; + margin-bottom: 5px; + padding: 10px 16px; + font-size: 17px; + width: 100%; + box-shadow: 0 0 0 1px var(--color-border),0 0 0 1px var(--color-border); + transition: box-shadow 150ms ease-in-out; + " + autofocus="" + autocomplete="off" + hx-get=(format!("{PILE_PREFIX}/search")) + hx-trigger="load, keyup changed delay:100ms" + hx-target="#search-results" + hx-swap="outerHTML" + hx-indicator="#search-spinner" + {} + } + + div id="search-spinner" class="htmx-indicator dot-spinner" { + span {} + span {} + span {} + } + + div id="search-results" {} + } + } + } + } + + div id="preview-overlay" { + img id="preview-overlay-img" src="" alt="" {} + } + + script { (maud::PreEscaped(" + function showPreview(el) { + var ov = document.getElementById('preview-overlay'); + var img = document.getElementById('preview-overlay-img'); + img.src = el.dataset.preview; + var rect = el.getBoundingClientRect(); + var size = 480; + var left = rect.right + 12; + if (left + size > window.innerWidth) { left = rect.left - size - 12; } + ov.style.left = left + 'px'; + ov.style.top = Math.max(8, rect.top - size / 2 + rect.height / 2) + 'px'; + ov.style.display = 'block'; + } + function hidePreview() { + var ov = document.getElementById('preview-overlay'); + ov.style.display = 'none'; + document.getElementById('preview-overlay-img').src = ''; + } + ")) } + } + }) +} diff --git a/crates/service/service-pile/src/pages/mod.rs b/crates/service/service-pile/src/pages/mod.rs new file mode 100644 index 0000000..5518a5b --- /dev/null +++ b/crates/service/service-pile/src/pages/mod.rs @@ -0,0 +1,37 @@ +use maud::html; +use reqwest::StatusCode; +use servable::{HtmlPage, PageMetadata}; +use service_assets::assets::{CSS_FIRA, CSS_FONTAWESOME}; +use std::sync::LazyLock; + +mod index; +pub use index::INDEX; + +use crate::{ASSET_PREFIX, PILE_PREFIX, routes::CSS_PILE}; + +pub static NOT_FOUND: LazyLock = LazyLock::new(|| { + HtmlPage::default() + .with_style_linked(CSS_PILE.route_at(PILE_PREFIX)) + .with_style_linked(CSS_FIRA.route_at(ASSET_PREFIX)) + .with_style_linked(CSS_FONTAWESOME.route_at(ASSET_PREFIX)) + .with_meta(PageMetadata { + title: "Page not found".into(), + author: None, + description: None, + image: None, + }) + .with_render(move |_page, _ctx| { + Box::pin(async { + html! { + div class="wrapper" { + div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh" { + p style="font-weight:bold;font-size:50pt;margin:0;" { "404" } + p style="font-size:13pt;margin:0;color:var(--grey);" { "(page not found)" } + a style="font-size:12pt;margin:10pt;padding:5px;" href="/" {"<- Back to site"} + } + } + } + }) + }) + .with_code(StatusCode::NOT_FOUND) +}); diff --git a/crates/service/service-pile/src/routes/mod.rs b/crates/service/service-pile/src/routes/mod.rs new file mode 100644 index 0000000..6188bcd --- /dev/null +++ b/crates/service/service-pile/src/routes/mod.rs @@ -0,0 +1,231 @@ +use axum::Router; +use axum::extract::{Query, State}; +use axum::routing::get; +use maud::{Markup, html}; +use pile_client::PileClient; +use servable::{CACHE_BUST_STR, ServableRouter, ServableWithRoute, StaticAsset}; +use std::sync::Arc; +use std::time::Instant; +use tower_http::compression::{CompressionLayer, DefaultPredicate}; +use tracing::warn; + +use crate::PILE_PREFIX; +use crate::pages; + +const PAGE_SIZE: usize = 50; + +pub(super) fn router(client: Arc) -> Router<()> { + let compression: CompressionLayer = CompressionLayer::new() + .br(true) + .deflate(true) + .gzip(true) + .zstd(true) + .compress_when(DefaultPredicate::new()); + + let search_router: Router<()> = Router::new() + .route("/search", get(search_handler)) + .with_state(client.clone()); + + let api_router: Router<()> = client.dataset("books").proxy_router(); + + build_server() + .into_router() + .merge(search_router) + .nest("/api", api_router) + .layer(compression) +} + +#[derive(serde::Deserialize)] +struct SearchQuery { + #[serde(default)] + q: String, + #[serde(default)] + page: usize, +} + +async fn search_handler( + State(client): State>, + Query(params): Query, +) -> Markup { + let start = Instant::now(); + let query = params.q.trim().to_lowercase(); + let page = params.page; + let mut query_invalid = false; + let mut list_error = false; + + let mut all_keys: Vec<(String, String)> = Vec::new(); + let mut filtered_total = 0; + let mut grand_total = 0; + + match query.is_empty() { + true => { + match client + .dataset("books") + .list_items(page * PAGE_SIZE, PAGE_SIZE) + .await + { + Err(error) => { + list_error = true; + warn!(message = "error while listing items", ?error); + } + + Ok(resp) => { + all_keys = resp + .items + .into_iter() + .map(|item| (item.source, item.key)) + .collect(); + filtered_total = resp.total; + grand_total = resp.total; + } + } + } + + false => match client.dataset("books").lookup(&query, Some(512)).await { + Err(_error) => { + query_invalid = true; + } + + Ok(resp) => { + let mut results = resp.results; + results.sort_unstable_by(|a, b| f32::total_cmp(&b.score, &a.score)); + + filtered_total = results.len(); + grand_total = results.len(); + all_keys = results.into_iter().map(|r| (r.source, r.key)).collect(); + } + }, + } + + // For empty query the server already paginated; for non-empty we slice locally. + let page_items: Vec<&(String, String)> = if query.is_empty() { + all_keys.iter().collect() + } else { + all_keys + .iter() + .skip(page * PAGE_SIZE) + .take(PAGE_SIZE) + .collect() + }; + + let has_more = (page + 1) * PAGE_SIZE < filtered_total; + let next_page = page + 1; + + let encoded_q: String = url::form_urlencoded::byte_serialize(query.as_bytes()).collect(); + let next_url = format!("{PILE_PREFIX}/search?q={}&page={}", encoded_q, next_page); + let elapsed_ms = start.elapsed().as_millis(); + + let mut msg = Vec::new(); + if query_invalid { + msg.push("invalid query"); + } + if list_error { + msg.push("list error"); + } + if filtered_total == 0 { + msg.push("no results"); + } + + if page == 0 { + html! { + div id="search-results" { + p class="search-meta" { + "Filtered " (filtered_total) "/" (grand_total) " items in " (elapsed_ms) "ms" + @if !msg.is_empty() { + span style="color:var(--orange)" { + (format!("\u{00A0}\u{00A0}({})", msg.join(", "))) + } + } + } + div class="result-grid" { + @for (source, key) in &page_items { + (result_item(source, key)) + } + @if has_more { + div + class="result-sentinel" + hx-get=(next_url) + hx-trigger="revealed" + hx-swap="outerHTML" + hx-target="this" + {} + } + } + } + } + } else { + html! { + @for (source, key) in &page_items { + (result_item(source, key)) + } + @if has_more { + div + class="result-sentinel" + hx-get=(next_url) + hx-trigger="revealed" + hx-swap="outerHTML" + hx-target="this" + {} + } + } + } +} + +fn result_item(source: &str, key: &str) -> Markup { + let enc_source: String = url::form_urlencoded::byte_serialize(source.as_bytes()).collect(); + let enc_key: String = url::form_urlencoded::byte_serialize(key.as_bytes()).collect(); + let enc_path: String = + url::form_urlencoded::byte_serialize("$.pdf.pages[0]".as_bytes()).collect(); + let thumb_url = + format!("{PILE_PREFIX}/api/field?source={enc_source}&key={enc_key}&path={enc_path}"); + let item_url = + format!("{PILE_PREFIX}/api/item?source={enc_source}&key={enc_key}&download=false"); + html! { + div class="result-item" { + div class="result-item-thumb" + data-preview=(thumb_url) + onmouseenter="showPreview(this)" + onmouseleave="hidePreview()" + { + img src=(thumb_url) alt="" onerror="this.style.visibility='hidden'" {} + } + div class="result-item-info" { + span class="result-item-key" + data-key=(key) + onclick="navigator.clipboard.writeText(this.dataset.key)" + title="Click to copy" + { (key) } + a class="result-item-link" href=(item_url) target="_blank" { "item" } + } + } + } +} + +pub static CSS_PILE: ServableWithRoute = ServableWithRoute::new( + || format!("/css/{}/main.css", *CACHE_BUST_STR), + StaticAsset { + bytes: grass::include!("crates/service/service-pile/css/main.scss").as_bytes(), + mime: mime::TEXT_CSS, + ttl: StaticAsset::DEFAULT_TTL, + }, +); + +fn build_server() -> ServableRouter { + ServableRouter::new() + .with_404(&pages::NOT_FOUND) + .add_page("/", &pages::INDEX) + // + .add_page_with_route(&CSS_PILE) +} + +#[test] +#[expect(clippy::unwrap_used)] +fn server_builds_without_panic() { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let _server = build_server(); + }); +}