From 95bb06aa4462c13333af613f4849ee00a7dc83c5 Mon Sep 17 00:00:00 2001 From: gennyble Date: Wed, 15 Jul 2026 23:59:10 -0500 Subject: awake is now a workspace --- Cargo.lock | 1644 ------------------------------------------------ Cargo.toml | 42 +- awake/Cargo.lock | 1644 ++++++++++++++++++++++++++++++++++++++++++++++++ awake/Cargo.toml | 39 ++ awake/src/atom/mod.rs | 154 +++++ awake/src/atom/urn.rs | 49 ++ awake/src/atomizer.rs | 163 +++++ awake/src/db.rs | 246 ++++++++ awake/src/error.rs | 28 + awake/src/fs.rs | 325 ++++++++++ awake/src/gatherer.rs | 429 +++++++++++++ awake/src/griph/mod.rs | 236 +++++++ awake/src/main.rs | 478 ++++++++++++++ awake/src/markup.rs | 409 ++++++++++++ awake/src/settings.rs | 7 + awake/src/templated.rs | 130 ++++ awake/src/timeparse.rs | 216 +++++++ awake/src/util.rs | 137 ++++ justfile | 7 +- src/atom/mod.rs | 154 ----- src/atom/urn.rs | 49 -- src/atomizer.rs | 163 ----- src/db.rs | 246 -------- src/error.rs | 28 - src/fs.rs | 325 ---------- src/gatherer.rs | 429 ------------- src/griph/mod.rs | 236 ------- src/main.rs | 478 -------------- src/markup.rs | 409 ------------ src/settings.rs | 7 - src/templated.rs | 130 ---- src/timeparse.rs | 216 ------- src/util.rs | 137 ---- 33 files changed, 4698 insertions(+), 4692 deletions(-) delete mode 100644 Cargo.lock mode change 100755 => 100644 Cargo.toml create mode 100644 awake/Cargo.lock create mode 100755 awake/Cargo.toml create mode 100644 awake/src/atom/mod.rs create mode 100644 awake/src/atom/urn.rs create mode 100755 awake/src/atomizer.rs create mode 100644 awake/src/db.rs create mode 100755 awake/src/error.rs create mode 100755 awake/src/fs.rs create mode 100644 awake/src/gatherer.rs create mode 100644 awake/src/griph/mod.rs create mode 100755 awake/src/main.rs create mode 100755 awake/src/markup.rs create mode 100755 awake/src/settings.rs create mode 100755 awake/src/templated.rs create mode 100755 awake/src/timeparse.rs create mode 100755 awake/src/util.rs delete mode 100644 src/atom/mod.rs delete mode 100644 src/atom/urn.rs delete mode 100755 src/atomizer.rs delete mode 100644 src/db.rs delete mode 100755 src/error.rs delete mode 100755 src/fs.rs delete mode 100644 src/gatherer.rs delete mode 100644 src/griph/mod.rs delete mode 100755 src/main.rs delete mode 100755 src/markup.rs delete mode 100755 src/settings.rs delete mode 100755 src/templated.rs delete mode 100755 src/timeparse.rs delete mode 100755 src/util.rs diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index d12a431..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,1644 +0,0 @@ -# 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 = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "async-trait" -version = "0.1.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "awake" -version = "0.1.0" -dependencies = [ - "axum", - "bempline", - "camino", - "confindent", - "cutie", - "gifed", - "rand", - "regex-lite", - "rusqlite", - "serde", - "sha2", - "snafu", - "time", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", - "url", -] - -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.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 = "bempline" -version = "0.10.0" -source = "git+https://github.com/gennyble/bempline#8b9d3030366dbb1c6909cfbf438ead9cd3377174" - -[[package]] -name = "bitflags" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bytemuck" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" - -[[package]] -name = "camino" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" - -[[package]] -name = "cc" -version = "1.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "confindent" -version = "3.0.0" -source = "git+https://github.com/gennyble/confindent?branch=v3.0-writing#cbb0aa4bc55c346f33e41f979e72cd3a95518a75" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cutie" -version = "0.1.0" -source = "git+https://git.dreamy.place/cutie#5ea6a13ead2a5cab0f578c8af5f0e0be88c3a08c" -dependencies = [ - "thiserror", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "foldhash" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" - -[[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 = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-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-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gifed" -version = "0.3.0" -source = "git+https://git.dreamy.place/multimedia/gifed/#176910e2ab605c6509969f557e8652cdfc6c3869" -dependencies = [ - "bitvec", - "color_quant", - "rgb", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[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.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[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 = "itoa" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "libsqlite3-sys" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4" -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 = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" -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", -] - -[[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 = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[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.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[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 = "redox_syscall" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rgb" -version = "0.8.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "rusqlite" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "time", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustversion" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" - -[[package]] -name = "ryu" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.138" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "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 = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" - -[[package]] -name = "snafu" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" -dependencies = [ - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "syn" -version = "2.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" -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" - -[[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 = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.3.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" - -[[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 = "valuable" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "weezl" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -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-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[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-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 = "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 = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[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 = "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/Cargo.toml b/Cargo.toml old mode 100755 new mode 100644 index 360c96b..94cbd7f --- a/Cargo.toml +++ b/Cargo.toml @@ -1,39 +1,3 @@ -[package] -name = "awake" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -axum = "0.7.4" -camino = "1.1.6" -snafu = "0.8.0" -bempline = { git = "https://github.com/gennyble/bempline" } -tokio = { version = "1.36.0", features = ["full"] } -tokio-util = { version = "0.7.10", features = ["io"] } -confindent = { git = "https://github.com/gennyble/confindent", branch = "v3.0-writing", features = [ - "macros", -] } -cutie = { git = "https://git.dreamy.place/cutie" } -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -time = { version = "0.3.36", features = [ - "macros", - "parsing", - "formatting", - "local-offset", -] } -sha2 = "0.10.8" -url = "2.5.0" -rusqlite = { version = "0.33.0", features = ["bundled", "time"] } -serde = { version = "1.0.217", features = ["derive"] } -gifed = { git = "https://git.dreamy.place/multimedia/gifed/", features = [ - "standard", -] } -regex-lite = "0.1.6" - -# for generating Atom URNs -[dependencies.rand] -version = "0.8.5" -features = ["std", "std_rng"] +[workspace] +resolve = "3" +members = ["awake"] diff --git a/awake/Cargo.lock b/awake/Cargo.lock new file mode 100644 index 0000000..d12a431 --- /dev/null +++ b/awake/Cargo.lock @@ -0,0 +1,1644 @@ +# 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 = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-trait" +version = "0.1.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "awake" +version = "0.1.0" +dependencies = [ + "axum", + "bempline", + "camino", + "confindent", + "cutie", + "gifed", + "rand", + "regex-lite", + "rusqlite", + "serde", + "sha2", + "snafu", + "time", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.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 = "bempline" +version = "0.10.0" +source = "git+https://github.com/gennyble/bempline#8b9d3030366dbb1c6909cfbf438ead9cd3377174" + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" + +[[package]] +name = "cc" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "confindent" +version = "3.0.0" +source = "git+https://github.com/gennyble/confindent?branch=v3.0-writing#cbb0aa4bc55c346f33e41f979e72cd3a95518a75" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cutie" +version = "0.1.0" +source = "git+https://git.dreamy.place/cutie#5ea6a13ead2a5cab0f578c8af5f0e0be88c3a08c" +dependencies = [ + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[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 = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-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-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gifed" +version = "0.3.0" +source = "git+https://git.dreamy.place/multimedia/gifed/#176910e2ab605c6509969f557e8652cdfc6c3869" +dependencies = [ + "bitvec", + "color_quant", + "rgb", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[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 = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libsqlite3-sys" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4" +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 = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +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", +] + +[[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 = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[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 = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "rusqlite" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "time", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "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 = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +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" + +[[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 = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[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 = "valuable" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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-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 = "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 = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[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 = "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/awake/Cargo.toml b/awake/Cargo.toml new file mode 100755 index 0000000..360c96b --- /dev/null +++ b/awake/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "awake" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = "0.7.4" +camino = "1.1.6" +snafu = "0.8.0" +bempline = { git = "https://github.com/gennyble/bempline" } +tokio = { version = "1.36.0", features = ["full"] } +tokio-util = { version = "0.7.10", features = ["io"] } +confindent = { git = "https://github.com/gennyble/confindent", branch = "v3.0-writing", features = [ + "macros", +] } +cutie = { git = "https://git.dreamy.place/cutie" } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +time = { version = "0.3.36", features = [ + "macros", + "parsing", + "formatting", + "local-offset", +] } +sha2 = "0.10.8" +url = "2.5.0" +rusqlite = { version = "0.33.0", features = ["bundled", "time"] } +serde = { version = "1.0.217", features = ["derive"] } +gifed = { git = "https://git.dreamy.place/multimedia/gifed/", features = [ + "standard", +] } +regex-lite = "0.1.6" + +# for generating Atom URNs +[dependencies.rand] +version = "0.8.5" +features = ["std", "std_rng"] diff --git a/awake/src/atom/mod.rs b/awake/src/atom/mod.rs new file mode 100644 index 0000000..2020163 --- /dev/null +++ b/awake/src/atom/mod.rs @@ -0,0 +1,154 @@ +mod urn; + +use std::str::FromStr; + +use bempline::{variables, Document, Options}; +use camino::{Utf8Path, Utf8PathBuf}; +use confindent::{let_get, Confindent, Entry, Node}; +use time::{format_description::well_known, OffsetDateTime}; +use urn::Urn; + +use crate::{markup, templated::Templated, timeparse}; + +pub fn main() -> ! { + let awake_conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap(); + + let root: Utf8PathBuf = awake_conf.child_parse("Webroot").unwrap(); + + // Grab the atom config and then grab the atom template + let atom_conf_path = std::env::args().nth(3).unwrap_or(String::from("atom.conf")); + let mut atom_conf = Confindent::from_file(&atom_conf_path).unwrap(); + let atom = Document::from_file( + root.clone() + .join(atom_conf.child_value("Template").unwrap()) + .canonicalize_utf8() + .unwrap(), + Options::default(), + ) + .unwrap(); + + let mut changed = false; + // Go through every feed and create the entries + let feeds = atom_conf.children_mut("Feed"); + for feed in feeds { + make_feed(feed, &mut changed, &root, atom.clone()) + } + + // If the atom has changed, probably because we created an ID + // write it to disk. + if changed { + std::fs::write(atom_conf_path, atom_conf.to_string()); + } + + std::process::exit(0); +} + +pub struct AtomFeed { + /// Where is the atom located in the webroot. (relative to webroot) + relpath: Utf8PathBuf, + /// What unique ID does this Feed have? + id: String, + + title: String, + subtitle: String, + updated: OffsetDateTime, + author_name: String, + author_email: String, + + entries: Vec, +} + +pub struct AtomEntry { + /// What file does this entry direct to? This file will be opened to inspect + /// the frontmatter for information to fill out the entry with. It will also + /// be used to create the link to the entry. + relpath: Utf8PathBuf, + /// What unique ID does this Entry have? + id: String, + + title: String, + published: OffsetDateTime, + updated: OffsetDateTime, +} + +/// Parses all the options of a feed and generates the build job +pub fn make_feed(feed: &mut Entry, changed: &mut bool, root: &Utf8Path, mut atom: Document) { + let_get!( + feed, + relpath: Utf8PathBuf = "Output", + clone id = "Id", + clone title = "Title", + clone subtitle = "Subtitle", + clone updated = "Updated", + ); + + //FIXME: gen- We need to backresolve the feed URL + let feed_link = ""; + let author_name = feed.get("Author/Name").unwrap().to_owned(); + let author_email = feed.get("Author/Email").unwrap().to_owned(); + + let mut atomfeed = AtomFeed { + relpath, + id, + title, + subtitle, + updated: timeparse::parse(&updated).unwrap(), + author_name, + author_email, + entries: vec![], + }; + + let entries = feed.children_mut("Entry"); + for entry in entries { + if !entry.has_child("Id") { + //TODO: gen- Read NID from conf? + let id = Urn::new_random("dreamy"); + entry.push_entry(("Id", id)); + *changed = true; + } + + atomfeed.entries.push(make_entry(entry, &root)); + } +} + +fn make_entry>(entry: &Entry, webroot: P) -> AtomEntry { + let relpath: Utf8PathBuf = entry.parse().unwrap(); + let path = webroot.as_ref().join(&relpath); + + let entry_content = match std::fs::read_to_string(&path) { + Ok(ok) => ok, + Err(e) => { + panic!("failed to get file at {path}: {e}"); + } + }; + let entry_templated = Templated::from_str(&entry_content).unwrap(); + + let conf_or_frontmatter = |conf_key: &str, front_key: &str| -> &str { + entry + .child_value(conf_key) + .unwrap_or_else(|| entry_templated.frontmatter.get(front_key).unwrap()) + }; + + let title = conf_or_frontmatter("Title", "title").to_owned(); + let id = entry.child_value("Id").unwrap().to_owned(); + + let entry_published_raw = conf_or_frontmatter("Published", "published"); + + let updated = timeparse::parse(entry.child_value("Updated").unwrap_or_else(|| { + entry_templated + .frontmatter + .get("updated") + .unwrap_or(entry_published_raw) + })) + .unwrap(); + + let published = timeparse::parse(entry_published_raw).unwrap(); + + AtomEntry { + relpath, + id, + title, + published, + updated, + } +} diff --git a/awake/src/atom/urn.rs b/awake/src/atom/urn.rs new file mode 100644 index 0000000..9ec73ae --- /dev/null +++ b/awake/src/atom/urn.rs @@ -0,0 +1,49 @@ +use core::fmt; + +use rand::{rngs::OsRng, Rng}; + +//TODO: gen- Check URN are valid +// https://www.rfc-editor.org/rfc/rfc2141#section-2.1 +pub struct Urn { + /// Namespace Identifier + nid: String, + /// Namespace Specific string. This is the unique part of the identifier + nss: String, +} + +impl Urn { + /// awake specific function for generating a URN in the form of: + /// `urn:NID:1234-5678` + /// Where 1234-5678 is a randomly generated eight digit ID broken in two. + pub fn new_random>(nid: S) -> Self { + let first = random_base58(4); + let second = random_base58(4); + + Self { + nid: nid.into(), + nss: format!("{first}-{second}"), + } + } +} + +impl fmt::Display for Urn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Urn { nid, nss } = self; + + write!(f, "urn:{nid}:{nss}") + } +} + +const BASE58: &'static [u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; +const USER_ID_LENGTH: usize = 6; +const SESSION_ID_LENGTH: usize = 12; + +/// Random Base58 string, `count` characters long, using OsRng which is assumed +/// to be secure +/// > assumed that system always provides high-quality cryptographically secure random data +fn random_base58(count: usize) -> String { + let mut rng = OsRng::default(); + std::iter::from_fn(|| Some(BASE58[rng.gen_range(0..BASE58.len())] as char)) + .take(count) + .collect() +} diff --git a/awake/src/atomizer.rs b/awake/src/atomizer.rs new file mode 100755 index 0000000..e6a147b --- /dev/null +++ b/awake/src/atomizer.rs @@ -0,0 +1,163 @@ +use std::str::FromStr; + +use bempline::{variables, Document, Options}; +use camino::{Utf8Path, Utf8PathBuf}; +use confindent::{Confindent, Value}; +use cutie::Html; +use time::format_description::well_known; + +use crate::{markup, templated::Templated, timeparse}; + +pub fn main() -> ! { + let awake_conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap(); + let atom_conf_path = std::env::args().nth(3).unwrap_or(String::from("atom.conf")); + + let atom_conf = Confindent::from_file(atom_conf_path).unwrap(); + + let root: Utf8PathBuf = awake_conf.child_parse("Webroot").unwrap(); + let atom = Document::from_file( + root.clone() + .join(atom_conf.child_value("Template").unwrap()) + .canonicalize_utf8() + .unwrap(), + Options::default(), + ) + .unwrap(); + + let feeds = atom_conf.children("Feed"); + for feed in feeds { + make_feed(feed, &root, atom.clone()) + } + + std::process::exit(0); +} + +pub fn make_feed(feed: &Value, root: &Utf8Path, mut atom: Document) { + let feed_relout: Utf8PathBuf = feed.child_parse("Output").unwrap(); + let feed_link = feed.child_owned("Link").unwrap(); + let feed_id = feed.child_owned("Id").unwrap(); + let feed_title = feed.child_owned("Title").unwrap(); + let feed_subtitle = feed.child_owned("Subtitle").unwrap(); + let feed_updated = feed.child_owned("Updated").unwrap(); + let feed_author_name = feed.get("Author/Name").unwrap(); + let feed_author_email = feed.get("Author/Email").unwrap(); + + variables!( + atom, + feed_link, + feed_id, + feed_title, + feed_subtitle, + feed_updated, + feed_author_email, + feed_author_name + ); + + let entries = feed.children("Entry"); + let entry_pattern = atom.get_pattern("entry").unwrap(); + for entry in entries { + let entry_relpath: Utf8PathBuf = entry.parse().unwrap(); + let entry_path = root.join(&entry_relpath); + + println!("entry {entry_relpath}"); + + let entry_content = match std::fs::read_to_string(&entry_path) { + Ok(ok) => ok, + Err(e) => { + panic!("failed to get file at {entry_path}: {e}"); + } + }; + let entry_templated = Templated::from_str(&entry_content).unwrap(); + + let entry_title = entry + .child_value("Title") + .unwrap_or_else(|| entry_templated.frontmatter.get("title").unwrap()); + let entry_link = entry.child_value("Link").unwrap(); + let entry_id = entry.child_value("Id").unwrap(); + + let entry_published_raw = entry + .child_value("Published") + .unwrap_or_else(|| entry_templated.frontmatter.get("published").unwrap()); + + let entry_updated = timeparse::parse(entry.child_value("Updated").unwrap_or_else(|| { + entry_templated + .frontmatter + .get("updated") + .unwrap_or(entry_published_raw) + })) + .unwrap() + .format(&well_known::Rfc3339) + .unwrap(); + + let entry_published = timeparse::parse(entry_published_raw) + .unwrap() + .format(&well_known::Rfc3339) + .unwrap(); + + let mut entry_content = markup::process(&entry_templated.content); + + if let Some(part_id) = entry + .child_value("Part") + .map(|raw| raw.strip_prefix('#')) + .flatten() + { + let cutie = cutie::Html::parse(entry_content); + let found = cutie.get_by_id(part_id).unwrap(); + entry_content = found.to_string(); + } + + let mut pattern = entry_pattern.clone(); + variables!( + pattern, + entry_title, + entry_link, + entry_id, + entry_published, + entry_updated + ); + pattern.set("entry_content", cdataify(&entry_content)); + atom.set_pattern("entry", pattern); + } + + let string = atom.compile(); + + let feed_out = root.join(feed_relout); + std::fs::write(feed_out, string).unwrap(); +} + +//FIXME: do NOT use this name +fn htmlspecialchars(raw: &str) -> String { + let mut ret = String::new(); + + for ch in raw.chars() { + match ch { + '<' => ret.push_str("<"), + '>' => ret.push_str(">"), + '&' => ret.push_str("&"), + '\'' => ret.push_str("'"), + '"' => ret.push_str("""), + c => ret.push(c), + } + } + + ret +} + +fn cdataify(raw: &str) -> String { + format!("") +} + +fn fix_links(html: &mut Html, path: Utf8PathBuf) { + let root = match path.file_name() { + Some(_) => path.parent().unwrap().to_owned(), + None => path, + }; + + for tag in html.child_tags_mut() { + if let Some(src) = tag.get_attribute("src") { + if !src.starts_with("http") { + todo!() + } + } + } +} diff --git a/awake/src/db.rs b/awake/src/db.rs new file mode 100644 index 0000000..0abf722 --- /dev/null +++ b/awake/src/db.rs @@ -0,0 +1,246 @@ +use std::sync::Mutex; + +use camino::Utf8PathBuf; +use rusqlite::{params, Connection, OptionalExtension}; +use time::OffsetDateTime; + +use crate::gatherer::{Cpuinfo, Meminfo}; + +pub struct Database { + db_path: Utf8PathBuf, + conn: Mutex, +} + +impl Database { + pub fn new(db_path: Utf8PathBuf) -> Self { + Self { + conn: Mutex::new(Connection::open(&db_path).unwrap()), + db_path, + } + } + + pub fn create_tables(&self) { + let conn = self.conn.lock().unwrap(); + conn.execute(CREATE_TABLE_HOSTMEM, params![]).unwrap(); + conn.execute(CREATE_TABLE_HOSTNET, params![]).unwrap(); + conn.execute(CREATE_TABLE_HOSTCPU, params![]).unwrap(); + } + + pub fn insert_host_meminfo(&self, meminfo: Meminfo) { + let conn = self.conn.lock().unwrap(); + + conn.execute( + "INSERT INTO stats_hostmem(total_kb, available_kb) VALUES (?1, ?2)", + params![meminfo.total, meminfo.avaialable], + ) + .unwrap(); + } + + pub fn get_last_host_meminfo(&self) -> DbMeminfo { + let conn = self.conn.lock().unwrap(); + + conn.query_row( + "SELECT * FROM stats_hostmem ORDER BY stamp DESC LIMIT 1", + [], + |row| { + let (stamp, total_kb, available_kb) = row.try_into().unwrap(); + + Ok(DbMeminfo { + stamp, + total_kb, + available_kb, + }) + }, + ) + .optional() + .unwrap() + .unwrap() + } + + pub fn get_last_n_host_meminfo(&self, count: usize) -> Vec { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn + .prepare("SELECT * FROM stats_hostmem ORDER BY stamp DESC LIMIT ?1") + .unwrap(); + + stmt.query_map(params![count], |row| { + Ok(DbMeminfo { + stamp: row.get(0)?, + total_kb: row.get(1)?, + available_kb: row.get(2)?, + }) + }) + .unwrap() + .map(|r| r.unwrap()) + .collect() + } + + pub fn get_date_bound_n_host_meminfo( + &self, + since: OffsetDateTime, + until: OffsetDateTime, + count: usize, + ) -> Vec { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn + .prepare("SELECT * FROM stats_hostmem WHERE stamp > ?1 AND stamp < ?2 ORDER BY stamp DESC LIMIT ?3") + .unwrap(); + + stmt.query_map(params![since, until, count], |row| { + Ok(DbMeminfo { + stamp: row.get(0)?, + total_kb: row.get(1)?, + available_kb: row.get(2)?, + }) + }) + .unwrap() + .map(|r| r.unwrap()) + .collect() + } + + pub fn insert_hostnet(&self, span_sec: usize, rx_delta: usize, tx_delta: usize) { + let conn = self.conn.lock().unwrap(); + + conn.execute( + "INSERT INTO stats_hostnet(span_sec, rx_delta, tx_delta) VALUES (?1, ?2, ?3)", + params![span_sec, rx_delta, tx_delta], + ) + .unwrap(); + } + + pub fn get_last_n_hostnet(&self, count: usize) -> Vec { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn + .prepare("SELECT * FROM stats_hostnet ORDER BY stamp DESC LIMIT ?1") + .unwrap(); + + stmt.query_map(params![count], |row| { + Ok(DbNetinfo { + stamp: row.get(0)?, + span_sec: row.get(1)?, + rx_delta: row.get(2)?, + tx_delta: row.get(3)?, + }) + }) + .unwrap() + .map(|r| r.unwrap()) + .collect() + } + + pub fn insert_hostcpu( + &self, + span_sec: usize, + user_delta: usize, + nice_delta: usize, + system_delta: usize, + ) { + let conn = self.conn.lock().unwrap(); + + conn.execute( + "INSERT INTO stats_hostcpu(span_sec, user_delta, nice_delta, system_delta) VALUES (?1, ?2, ?3, ?4)", + params![span_sec, user_delta, nice_delta, system_delta], + ) + .unwrap(); + } + + pub fn get_last_n_hostcpu(&self, count: usize) -> Vec { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn + .prepare("SELECT * FROM stats_hostcpu ORDER BY stamp DESC LIMIT ?1") + .unwrap(); + + stmt.query_map(params![count], |row| { + Ok(DbCpuinfo { + stamp: row.get(0)?, + span_sec: row.get(1)?, + user_delta: row.get(2)?, + nice_delta: row.get(3)?, + system_delta: row.get(4)?, + }) + }) + .unwrap() + .map(|r| r.unwrap()) + .collect() + } +} + +pub const CREATE_TABLE_HOSTMEM: &'static str = "\ + CREATE TABLE IF NOT EXISTS stats_hostmem ( + stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + total_kb INTEGER NOT NULL, + available_kb INTEGER NOT NULL + );"; + +pub const CREATE_TABLE_HOSTNET: &'static str = "\ + CREATE TABLE IF NOT EXISTS stats_hostnet( + stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + span_sec INTEGER NOT NULL, + rx_delta INTEGER NOT NULL, + tx_delta INTEGER NOT NULL + );"; + +pub const CREATE_TABLE_HOSTCPU: &'static str = "\ + CREATE TABLE IF NOT EXISTS stats_hostcpu( + stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + span_sec INTEGER NOT NULL, + user_delta INTEGER NOT NULL, + nice_delta INTEGER NOT NULL, + system_delta INTEGER NOT NULL + );"; + +#[derive(Clone, Copy, Debug)] +pub struct DbMeminfo { + pub stamp: OffsetDateTime, + pub total_kb: usize, + pub available_kb: usize, +} + +impl DbMeminfo { + pub fn usage(&self) -> usize { + self.total_kb - self.available_kb + } +} + +#[derive(Copy, Clone, Debug)] +pub struct DbNetinfo { + pub stamp: OffsetDateTime, + pub span_sec: usize, + pub rx_delta: usize, + pub tx_delta: usize, +} + +impl DbNetinfo { + pub fn rx_bytes_per_sec(&self) -> f32 { + self.rx_delta as f32 / self.span_sec as f32 + } + + pub fn tx_bytes_per_sec(&self) -> f32 { + self.tx_delta as f32 / self.span_sec as f32 + } +} + +#[derive(Copy, Clone, Debug)] +pub struct DbCpuinfo { + pub stamp: OffsetDateTime, + pub span_sec: usize, + pub user_delta: usize, + pub nice_delta: usize, + pub system_delta: usize, +} + +impl DbCpuinfo { + /// Returns the avarage usage still in USER_HZ which is + /// in 1/100ths sec on "most systems". This is averaged + /// across the minute sample rate, so unit is: + /// USER_HZ/s + pub fn average_usage(&self) -> f32 { + let used_sum = self.user_delta + self.nice_delta + self.system_delta; + let avg = used_sum as f32 / self.span_sec as f32; + avg + } + + /// The sum of the user, nice, and system deltas + pub fn usage_sum(&self) -> usize { + self.user_delta + self.nice_delta + self.system_delta + } +} diff --git a/awake/src/error.rs b/awake/src/error.rs new file mode 100755 index 0000000..56539d0 --- /dev/null +++ b/awake/src/error.rs @@ -0,0 +1,28 @@ +use std::io; + +use camino::Utf8PathBuf; + +#[derive(Debug, snafu::Snafu)] +pub enum RuntimeError { + #[snafu(display("the path was not found: {path}"))] + NotFound { + source: io::Error, + path: Utf8PathBuf, + }, + #[snafu(display("io error: {path}: {source}"))] + UnknownIo { + source: io::Error, + path: Utf8PathBuf, + }, + #[snafu(display("path tried to go below webroot: {path}"))] + PathTooLow { path: String }, +} + +impl RuntimeError { + pub fn from_io(source: io::Error, path: Utf8PathBuf) -> Self { + match source.kind() { + io::ErrorKind::NotFound => RuntimeError::NotFound { source, path }, + _ => RuntimeError::UnknownIo { source, path }, + } + } +} diff --git a/awake/src/fs.rs b/awake/src/fs.rs new file mode 100755 index 0000000..e8044a0 --- /dev/null +++ b/awake/src/fs.rs @@ -0,0 +1,325 @@ +use camino::{Utf8Path, Utf8PathBuf}; +use std::{fmt, io, ops::Deref, path::Display, str::FromStr}; + +use crate::RuntimeError; + +/// Webpath is the path we get from HTTP requests. It's garunteed to not fall +/// below the webroot and will never start or end with a slash. +#[derive(Clone, Debug, PartialEq)] +pub struct Webpath { + pub webcanon: Utf8PathBuf, + is_dir: bool, +} + +impl FromStr for Webpath { + type Err = RuntimeError; + + fn from_str(raw: &str) -> Result { + let is_dir = raw.trim_end().ends_with('/'); + let mut curr = Utf8PathBuf::new(); + + for component in raw.split('/') { + match component { + "." => continue, + ".." => { + if !curr.pop() { + return Err(RuntimeError::PathTooLow { + path: raw.to_owned(), + }); + } + } + comp => curr.push(comp), + } + } + + Ok(Self { + webcanon: curr, + is_dir, + }) + } +} + +impl Webpath { + /// Return whether or not this Webpath is empty which would indicate it's + /// the homepage. + pub fn is_index(&self) -> bool { + self.webcanon == "" + } + + /// Whether or not this Webpath points to a directory (has a trailing slash) + pub fn is_dir(&self) -> bool { + self.is_dir + } + + /// Return a String that interprets this Webpath as a directory, adding a + /// trailing slash + pub fn as_dir(&self) -> String { + format!("/{}/", self.webcanon) + } + + /// Returns the first directory of the Webpath. Assumes the path is a + /// directory if it ends in a `/`, otherwise pops the last component. + pub fn first_dir(&self) -> String { + if self.is_dir { + self.webcanon.to_string() + } else { + let mut dir = self.webcanon.clone(); + dir.pop(); + dir.to_string() + } + } +} + +impl Deref for Webpath { + type Target = Utf8Path; + + fn deref(&self) -> &Self::Target { + &self.webcanon + } +} + +impl AsRef for Webpath { + fn as_ref(&self) -> &Utf8Path { + &self.webcanon + } +} + +impl PartialEq for Webpath { + fn eq(&self, other: &str) -> bool { + self.webcanon.eq(other) + } +} + +impl fmt::Display for Webpath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "/{}", self.webcanon) + } +} + +const ROOT_INDEX: &str = "home.html"; +const DIRFILE_EXT: &str = "html"; + +pub struct PathResolution { + pub filepath: Utf8PathBuf, + pub is_dirfile: bool, +} + +impl Deref for PathResolution { + type Target = Utf8Path; + + fn deref(&self) -> &Self::Target { + self.filepath.as_path() + } +} + +impl fmt::Display for PathResolution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.deref()) + } +} + +#[derive(Clone, Debug)] +pub struct Filesystem { + webroot: Utf8PathBuf, +} + +impl Filesystem { + pub fn new>(root: P) -> Self { + Self { + webroot: root.into(), + } + } + + pub fn resolve(&self, webpath: &Webpath) -> Result { + if webpath.is_index() || webpath == ROOT_INDEX { + return Ok(PathResolution { + filepath: self.webroot.join(ROOT_INDEX), + is_dirfile: false, + }); + } + + let path = self.webroot.join(webpath); + let metadata = Self::metadata(&path)?; + + // If it's a file then return the path immediatly + if metadata.is_file() { + Ok(PathResolution { + filepath: path, + is_dirfile: false, + }) + } else { + // we check this above. but still.. + //TODO: gen- probably don't unwrap + let filename = path.file_name().unwrap(); + let dirfile = path.join(format!("{filename}.{DIRFILE_EXT}")); + + if dirfile.exists() { + Ok(PathResolution { + filepath: dirfile, + is_dirfile: true, + }) + } else { + Err(RuntimeError::NotFound { + source: io::ErrorKind::NotFound.into(), + path: dirfile, + }) + } + } + } + + /// Resolve a file system path to a webpath. + /// + /// Paths are not checked to see if they exist and are not checked to be within the webroot. + /// + /// Paths attempt to resolve to the simplest form. For example, if the path is [`ROOT_INDEX`] + /// it will come out as `/` and if it is a dirfile, it will come out as a directory. + /// + /// **FOOTGUN** currently assumes every path is a file... + pub fn reverse_resolve>(&self, path: P) -> Result { + let path = path.as_ref(); + let relpath = match path.strip_prefix(&self.webroot) { + Ok(relpath) => relpath, + Err(_e) => { + if path.is_relative() { + path + } else { + //FIXME: gen- This error is not strictly correct, but it'll do for now. + // 2024-10-12 + return Err(RuntimeError::PathTooLow { + path: path.to_string(), + }); + } + } + }; + + if relpath == "" || relpath == ROOT_INDEX { + return Ok(Webpath { + webcanon: Utf8PathBuf::new(), + is_dir: true, + }); + } + + todo!(); + + Ok(Webpath { + webcanon: relpath.into(), + is_dir: false, + }) + } + + pub fn metadata>(path: P) -> Result { + path.as_ref() + .metadata() + .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) + } + + pub async fn open>(path: P) -> Result { + tokio::fs::File::open(path.as_ref()) + .await + .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) + } + + pub async fn read>(path: P) -> Result, RuntimeError> { + tokio::fs::read(path.as_ref()) + .await + .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) + } + + pub async fn read_to_string>(path: P) -> Result { + tracing::trace!("read_to_string {}", path.as_ref()); + + tokio::fs::read_to_string(path.as_ref()) + .await + .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use crate::fs::{Webpath, ROOT_INDEX}; + + use super::Filesystem; + + macro_rules! webpath { + ($location:expr) => { + Webpath::from_str($location).unwrap() + }; + } + + #[test] + fn webpath_finds_too_low() { + assert!(Webpath::from_str("/..").is_err()); + assert!(Webpath::from_str("/one/..").is_ok()); + assert!(Webpath::from_str("/one/../..").is_err()); + assert!(Webpath::from_str("/one/../two").is_ok()) + } + + #[test] + fn webpath_path_correct() { + assert_eq!(Webpath::from_str("/one").unwrap().webcanon, "one"); + assert_eq!(Webpath::from_str("/one/..").unwrap().webcanon, ""); + assert_eq!(Webpath::from_str("/one/two/..").unwrap().webcanon, "one"); + assert_eq!(Webpath::from_str("/one/../two").unwrap().webcanon, "two"); + assert_eq!( + Webpath::from_str("/one/two/three/..").unwrap().webcanon, + "one/two" + ); + assert_eq!( + Webpath::from_str("/one/../home.html").unwrap().webcanon, + "home.html" + ); + } + + const TESTROOT: &str = "test/serve"; + + #[test] + fn filesystem_resolves_index() { + let fs = Filesystem::new(TESTROOT); + assert_eq!( + fs.resolve(&webpath!("/")).unwrap().filepath, + format!("{TESTROOT}/{ROOT_INDEX}") + ); + assert_eq!( + fs.resolve(&webpath!("/one/..")).unwrap().filepath, + format!("{TESTROOT}/{ROOT_INDEX}") + ); + } + + #[test] + fn filesystem_resolves_dirfile() { + let fs = Filesystem::new(TESTROOT); + assert_eq!( + fs.resolve(&webpath!("/one")).unwrap().filepath, + format!("{TESTROOT}/one/one.html") + ); + assert_eq!( + fs.resolve(&webpath!("/one/eleven/")).unwrap().filepath, + format!("{TESTROOT}/one/eleven/eleven.html") + ); + assert_eq!( + fs.resolve(&webpath!("/one/eleven/..")).unwrap().filepath, + format!("{TESTROOT}/one/one.html") + ); + } + + #[test] + fn filesystem_reverse_resolves_index() { + let fs = Filesystem::new(TESTROOT); + + assert_eq!(fs.reverse_resolve(TESTROOT).unwrap(), webpath!("/")); + assert_eq!( + fs.reverse_resolve(format!("{TESTROOT}/{ROOT_INDEX}")) + .unwrap(), + webpath!("/") + ) + } + + #[test] + fn filesystem_reverse_resolves_directories() { + let fs = Filesystem::new(TESTROOT); + + assert_eq!(fs.reverse_resolve("test/").unwrap(), webpath!("test/")) + } +} diff --git a/awake/src/gatherer.rs b/awake/src/gatherer.rs new file mode 100644 index 0000000..e6bae6f --- /dev/null +++ b/awake/src/gatherer.rs @@ -0,0 +1,429 @@ +use std::{ + array, + fs::File, + io::{BufRead, BufReader}, + sync::{atomic::Ordering, mpsc::Sender}, + thread::JoinHandle, + time::Duration, +}; + +use regex_lite::Regex; +use time::OffsetDateTime; + +use crate::{ + db::DbMeminfo, + griph::{self, Style, TwoLineOrder}, + AwakeState, +}; + +pub struct Gatherer { + state: AwakeState, + hwnd: Option>, +} + +impl Gatherer { + pub fn new(state: AwakeState) -> Self { + Self { state, hwnd: None } + } + + pub fn start(&mut self) { + let state = self.state.clone(); + let hwnd = std::thread::spawn(|| task(state)); + self.hwnd = Some(hwnd); + } +} + +fn task(state: AwakeState) { + tracing::info!("starting gatherer thread"); + + if !state.do_statistics { + tracing::warn!("statistics disabled"); + return; + } + + // I just want a graph on first boot; don't care about divisions just yet + make_mem_graph(&state); + make_net_graph(&state); + make_cpu_graph(&state); + + // If we collected a point less than a minute ago, like after + // just being restarted, wait until it's been a minute + let last_meminfo = state.database.get_last_host_meminfo(); + let since = OffsetDateTime::now_utc() - last_meminfo.stamp; + if since < time::Duration::minutes(1) { + let to_minute = time::Duration::minutes(1) - since; + + tracing::info!( + "waiting for {}s to space a minute apart", + to_minute.whole_seconds() + ); + std::thread::sleep(to_minute.try_into().unwrap()); + } + + let mut last_netinfo: Option = None; + let mut last_cpuinfo: Option = None; + + // this is a `let _` because otherwise the attribute was + // making the comiler mad + #[rustfmt::skip] + let _ = loop { + tracing::debug!("collecting stats"); + + // Gather data + let meminfo = Meminfo::current(); + let netinfo = Netinfo::current(); + let cpuinfo = Cpuinfo::current(); + + // Print traces, y'know, for tracing + tracing::trace!("memory: {}MB used / {}MB total", meminfo.usage() / 1000, meminfo.total / 1000); + tracing::trace!("net: rx {} / tx {}", data_human_fmt(netinfo.rx_bytes), data_human_fmt(netinfo.tx_bytes)); + tracing::trace!("cpu: user {} // nice {} // system {}", cpuinfo.user, cpuinfo.nice, cpuinfo.system); + + // Store stats in database + if let Some(lni) = last_netinfo { + let rx_delta = netinfo.rx_bytes - lni.rx_bytes; + let tx_delta = netinfo.tx_bytes - lni.tx_bytes; + + state.database.insert_hostnet(60, rx_delta, tx_delta); + } + last_netinfo = Some(netinfo); + + if let Some(lci) = last_cpuinfo { + let user_delta = cpuinfo.user - lci.user; + let nice_delta = cpuinfo.nice - lci.nice; + let system_delta = cpuinfo.system - lci.system; + + state.database.insert_hostcpu(60, user_delta, nice_delta, system_delta); + } + last_cpuinfo = Some(cpuinfo); + + state.database.insert_host_meminfo(meminfo); + + // Only generate graphs every 15 minutes + let now = OffsetDateTime::now_utc(); + if now.minute() % 15 == 0 { + make_mem_graph(&state); + make_net_graph(&state); + make_cpu_graph(&state); + } + + std::thread::sleep(Duration::from_secs(60)); + }; +} + +pub fn make_mem_graph(state: &AwakeState) { + tracing::debug!("generating meminfo graph"); + + let infos = state.database.get_last_n_host_meminfo(256); + let max = infos[0].total_kb; + + let now = get_clean_now(); + let cleaned = clean_series(&infos, |mem| mem.stamp, now); + let mut usages: Vec> = cleaned + .into_iter() + .map(|mi| mi.map(|mi| mi.usage())) + .collect(); + + // Reversing here because we want latest valeus on on the + // right side, so last in the array + usages.reverse(); + + let gif = griph::make_1line(0, max, &usages, Style::Discontinuous); + + let path = state.cache_path.join("current_hostmeminfo.gif"); + gif.save(path).unwrap(); +} + +const NET_INCREMENTS: usize = 256; + +pub fn make_net_graph(state: &AwakeState) { + tracing::debug!("generating netinfo graph"); + + let now = get_clean_now(); + let infos = state.database.get_last_n_hostnet(256); + let cleaned = clean_series(&infos, |net| net.stamp, now); + + // 125 is (1000 / 8) so it converst Bytes to kiloBITS + let mut rx_deltas = extract(&cleaned, |ni| ni.rx_bytes_per_sec() as usize / 125); + let mut tx_deltas = extract(&cleaned, |ni| ni.tx_bytes_per_sec() as usize / 125); + + // Reversing to put latest values on the right side + rx_deltas.reverse(); + tx_deltas.reverse(); + + let rx_zeroed: Vec = cleaned + .iter() + .map(|m| m.map(|n| n.rx_bytes_per_sec() as usize / 125).unwrap_or(0)) + .collect(); + + let tx_zeroed: Vec = cleaned + .iter() + .map(|m| m.map(|n| n.tx_bytes_per_sec() as usize / 125).unwrap_or(0)) + .collect(); + + let rx_sum = rx_zeroed.iter().fold(0, |acc, x| acc + x); + let tx_sum = tx_zeroed.iter().fold(0, |acc, x| acc + x); + + // Mixing the TX/RX delta so we can pick a range. + let mut mixed = vec![0; 512]; + mixed[..256].copy_from_slice(&rx_zeroed); + mixed[256..].copy_from_slice(&tx_zeroed); + + mixed.sort(); + let kinda_highest = mixed[511 - 32]; + // We add one here so we have less "surface area" covered by the lines + // which makes the vertical divisions look btter + let high_bound_whole = (kinda_highest as f32 / NET_INCREMENTS as f32) + .ceil() + .max(1.0) as usize; + + let high_bound = if high_bound_whole > 1 { + high_bound_whole + 1 + } else { + high_bound_whole + } * NET_INCREMENTS; + + state + .netinfo_upper_bound + .store(high_bound, Ordering::Release); + + let order = if tx_sum > rx_sum { + TwoLineOrder::SeriesAFirst + } else { + TwoLineOrder::SeriesBFirst + }; + + tracing::debug!("tx_sum = {tx_sum} // rx_sum = {rx_sum} // order = {order:?}"); + + /*for (idx, (tx, rx)) in tx_deltas.iter().zip(rx_deltas).enumerate() { + println!( + "[{idx:<03}] tx = {} / rx = {}", + tx.unwrap_or_default(), + rx.unwrap_or_default() + ); + }*/ + + let gif = griph::make_2line(0, high_bound, &tx_deltas, &rx_deltas, order); + + let path = state.cache_path.join("current_hostnetinfo.gif"); + gif.save(path).unwrap(); +} + +pub fn make_cpu_graph(state: &AwakeState) { + tracing::debug!("generating cpuinfo graph"); + + let infos = state.database.get_last_n_hostcpu(256); + + let now = get_clean_now(); + let cleaned = clean_series(&infos, |cpu| cpu.stamp, now); + + // Usages are of the unit of hundreths of a second of CPU usage averaged over a minute. + // A value of 1 means the cpu saw 1% CPU usage + let usages = extract(&cleaned, |cpu| cpu.average_usage()); + + let mut zeroed: Vec = usages.iter().map(|m| m.unwrap_or(0.0)).collect(); + zeroed.sort_by(|a, b| a.partial_cmp(&b).unwrap()); + let kinda_highest = zeroed[255 - 8]; + + tracing::debug!( + "kinda_highest = {kinda_highest} // highest = {}", + zeroed[255] + ); + + // high_vound unit: hundreths of a second (1% / 1) + // scaling by 1.33 here to drive the line down to decrease the apparent + // "surface area" of the line + let high_bound = (kinda_highest * 1.33).max(1.0); + state + .cpuinfo_upper_bound + .store(high_bound as usize, Ordering::Release); + + // can't scale the range, so we have to scale the inputs. + // Finally, we multiply by ten to put it in a 0-1000 range + let scale_factor = 100.0 / high_bound; + let mut scaled: Vec> = usages + .iter() + .map(|m| m.map(|v| (v * scale_factor) as usize * 10)) + .collect(); + + // Reversing here because we want latest valeus on on the + // right side, so last in the array + scaled.reverse(); + + let gif = griph::make_1line(0, 1000, &scaled, Style::Line); + + let path = state.cache_path.join("current_hostcpuinfo.gif"); + gif.save(path).unwrap(); +} + +fn get_clean_now() -> OffsetDateTime { + OffsetDateTime::now_utc().replace_second(0).unwrap() +} + +fn clean_series(series: &[T], time_extractor: F, end_time: OffsetDateTime) -> [Option; 256] +where + F: Fn(&T) -> OffsetDateTime, + T: Clone, +{ + let mut res = [const { None }; 256]; + + for value in series { + let time = time_extractor(value); + let delta = end_time - time; + let mins = delta.whole_minutes(); + + if mins >= 0 && mins < 256 { + res[mins as usize] = Some(value.clone()); + } + } + + res +} + +fn extract(series: &[Option], extractor: F) -> [Option; 256] +where + F: Fn(&T) -> V, +{ + let mut res = [const { None }; 256]; + for (idx, maybe) in series.iter().enumerate() { + if let Some(value) = maybe { + res[idx] = Some(extractor(value)); + } + } + + res +} + +pub struct Meminfo { + pub total: usize, + pub free: usize, + pub avaialable: usize, +} + +impl Meminfo { + pub fn current() -> Self { + let procinfo = File::open("/proc/meminfo").unwrap(); + let bread = BufReader::new(procinfo); + + let mut meminfo = Meminfo { + total: 0, + free: 0, + avaialable: 0, + }; + + for line in bread.lines() { + let line = line.unwrap(); + + if let Some((raw_key, raw_value_kb)) = line.split_once(':') { + let value = if let Some(raw_value) = raw_value_kb.trim().strip_suffix(" kB") { + if let Ok(parsed) = raw_value.parse() { + parsed + } else { + continue; + } + } else { + continue; + }; + + match raw_key.trim() { + "MemTotal" => meminfo.total = value, + "MemFree" => meminfo.free = value, + "MemAvailable" => meminfo.avaialable = value, + _ => (), + } + } + } + + meminfo + } + + pub fn usage(&self) -> usize { + self.total - self.avaialable + } +} + +pub struct Netinfo { + rx_bytes: usize, + tx_bytes: usize, +} + +impl Netinfo { + pub fn current() -> Self { + let procinfo = File::open("/proc/net/dev").unwrap(); + let bread = BufReader::new(procinfo); + + let mut netinfo = Self { + rx_bytes: 0, + tx_bytes: 0, + }; + + let interface = "eth0:"; + for line in bread.lines() { + let line = line.unwrap(); + let trim = line.trim(); + + if let Some(data) = trim.strip_prefix(interface) { + let mut splits = data.split_whitespace(); + netinfo.rx_bytes = splits.next().unwrap().parse().unwrap(); + netinfo.tx_bytes = splits.skip(7).next().unwrap().parse().unwrap(); + break; + } + } + + netinfo + } +} + +/// Not strictly the correct name for this, but that's fine. +/// Structure for parsed-from /proc/stat +pub struct Cpuinfo { + user: usize, + nice: usize, + system: usize, +} + +impl Cpuinfo { + pub fn current() -> Self { + let procinfo = File::open("/proc/stat").unwrap(); + let bread = BufReader::new(procinfo); + + let mut cpuinfo = Cpuinfo { + user: 0, + nice: 0, + system: 0, + }; + + for line in bread.lines() { + let line = line.unwrap(); + let Some(data) = line.strip_prefix("cpu ") else { + continue; + }; + + let mut splits = data.split(' '); + cpuinfo.user = splits.next().unwrap().parse().unwrap(); + cpuinfo.nice = splits.next().unwrap().parse().unwrap(); + cpuinfo.system = splits.next().unwrap().parse().unwrap(); + } + + cpuinfo + } +} + +fn data_human_fmt(bytes: usize) -> String { + let (num, unit) = data_human(bytes); + format!("{num}{unit}") +} + +fn data_human(bytes: usize) -> (f32, &'static str) { + const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB"]; + + let mut wrk = bytes as f32; + let mut unit_idx = 0; + loop { + if wrk < 1500.0 || unit_idx == UNITS.len() - 1 { + return (wrk, UNITS[unit_idx]); + } + wrk /= 1000.0; + unit_idx += 1; + } +} diff --git a/awake/src/griph/mod.rs b/awake/src/griph/mod.rs new file mode 100644 index 0000000..ef2d1ee --- /dev/null +++ b/awake/src/griph/mod.rs @@ -0,0 +1,236 @@ +use gifed::{writer::ImageBuilder, Gif, StandardGif}; + +#[rustfmt::skip] +pub const DARK_PALETTE: &[u8] = &[ + 0, 0, 0, // Background - Black + 192, 192, 192, // Graphline - Mostly White + 64, 64, 64, // Gridlines - Dark gray + 32, 32, 32, // Minor Gridlines - Darker gray + 144, 144, 255, // Primary 2 Colour - Blue + 48, 192, 48, // Secondary 2 Colour - Green + 96, 96, 224, // Primary Underfill - Light Blue + 48, 128, 48, // Secondary Underfill - Lesser Green + 255, 255, 255, // Graphline Underfill - 16 * 9 gray + 192, 128, 128 // Missing Data Red - lightish red +]; + +const BACKGROUND: u8 = 0; +const LINE: u8 = 1; +const GRIDLINE: u8 = 2; +const MINOR_GRIDLINE: u8 = 3; +const LINE1: u8 = 4; +const LINE2: u8 = 5; +const LINE1_FILL: u8 = 6; +const LINE2_FILL: u8 = 7; +const LINE_FILL: u8 = 8; +const MISSING: u8 = 9; + +const WIDTH: usize = 256; +const HEIGHT: usize = 160; +const SIZE: usize = WIDTH * HEIGHT; + +pub enum Style { + Discontinuous, + Line, + UnderfilledLine, +} + +pub fn make_1line(min: usize, max: usize, values: &[Option], style: Style) -> Gif { + let range = max - min; + // this assumes a range of values that is >1 per pixel + let vpp = range as f32 / HEIGHT as f32; + + let mut raster = vec![0; SIZE]; + let mut standard = Gif::new(WIDTH as u16, HEIGHT as u16); + + draw_grid(&mut raster); + draw_line(&mut raster, values, vpp, LINE); + + let plt = match style { + Style::Discontinuous => { + draw_line(&mut raster, values, vpp, LINE); + DARK_PALETTE //[0..12] + } + Style::Line => { + draw_line_continuous(&mut raster, values, vpp, LINE); + DARK_PALETTE //[0..12] + } + Style::UnderfilledLine => { + draw_line_underfill(&mut raster, values, vpp, LINE_FILL, LINE); + DARK_PALETTE + } + }; + + standard.set_palette(Some(plt.try_into().unwrap())); + standard.push( + ImageBuilder::new(WIDTH as u16, HEIGHT as u16) + .build(raster) + .unwrap(), + ); + + standard +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum TwoLineOrder { + SeriesAFirst, + SeriesBFirst, +} + +pub fn make_2line( + min: usize, + max: usize, + values1: &[Option], + values2: &[Option], + draw_order: TwoLineOrder, +) -> Gif { + let range = max - min; + // this assumes a range of values that is >1 per pixel + let vpp = range as f32 / HEIGHT as f32; + + let mut raster = vec![0; SIZE]; + draw_grid(&mut raster); + + match draw_order { + TwoLineOrder::SeriesAFirst => { + draw_line_underfill(&mut raster, values1, vpp, LINE1, LINE1_FILL); + draw_line_underfill(&mut raster, values2, vpp, LINE2, LINE2_FILL); + } + TwoLineOrder::SeriesBFirst => { + draw_line_underfill(&mut raster, values2, vpp, LINE2, LINE2_FILL); + draw_line_underfill(&mut raster, values1, vpp, LINE1, LINE1_FILL); + } + } + + let mut standard = Gif::new(WIDTH as u16, HEIGHT as u16); + standard.set_palette(Some(DARK_PALETTE.try_into().unwrap())); + standard.push( + ImageBuilder::new(WIDTH as u16, HEIGHT as u16) + .build(raster) + .unwrap(), + ); + + standard +} + +fn draw_grid(raster: &mut [u8]) { + // Draw Divisions + // we want a gridline every 16 pixels, but not the bottom + // or top, so only 8. + for div in 1..=9 { + let y_val = div * 16; + + let grid = if div % 2 == 0 { + GRIDLINE + } else { + MINOR_GRIDLINE + }; + + for x in 0..WIDTH { + raster[y_val * WIDTH + x] = grid; + } + } + + // Draw vertical divisions every 30 pixels (minutes) + for div in 1..=8 { + let x_val = WIDTH - (div * 30); + + for y in 0..HEIGHT { + raster[y * WIDTH + x_val] = GRIDLINE; + } + } +} + +fn draw_line(raster: &mut [u8], values: &[Option], vpp: f32, colour: u8) { + // Draw Line + // this will be discontinuous and i think that's okay. we + // could make it a proper line by keeping track what value + // was last and drawing the whole vertical there + for (x, maybe) in values.iter().enumerate() { + if let Some(value) = maybe { + let value_height = (*value as f32 / vpp) as usize; + if value_height > (HEIGHT - 1) { + //TODO: this causes too-high values not to draw at all; gaps + continue; + } + let y_val = (HEIGHT - 1) - value_height; + + raster[y_val * WIDTH + x] = colour; + } else { + draw_missing(raster, x); + } + } +} + +fn draw_line_continuous(raster: &mut [u8], values: &[Option], vpp: f32, colour: u8) { + let mut last_value = None; + + for (x, maybe) in values.iter().enumerate() { + if let Some(value) = maybe { + let value_height = (*value as f32 / vpp) as usize; + let overflow = value_height > HEIGHT - 1; + let y_val = (HEIGHT - 1) - value_height.min(HEIGHT - 1); + + match last_value { + Some(last) if last > y_val => { + for y_offset in y_val..last { + raster[y_offset * WIDTH + x] = colour; + } + raster[y_val * WIDTH + x] = colour; + } + Some(last) if last < y_val => { + for y_offset in last..y_val { + raster[y_offset * WIDTH + x - 1] = colour; + } + raster[y_val * WIDTH + x] = colour; + } + _ => { + // This is the None case and the equal value case + raster[y_val * WIDTH + x] = colour; + } + } + + last_value = Some(y_val); + } else { + draw_missing(raster, x); + + last_value = None; + } + } +} + +fn draw_line_underfill( + raster: &mut [u8], + values: &[Option], + vpp: f32, + colour: u8, + colour_fill: u8, +) { + for (x, maybe) in values.iter().enumerate() { + if let Some(value) = maybe { + let value_height = (*value as f32 / vpp) as usize; + let overflow = value_height > HEIGHT - 1; + let y_val = (HEIGHT - 1) - value_height.min(HEIGHT - 1); + + for y in y_val..HEIGHT { + raster[y * WIDTH + x] = colour_fill; + } + + if !overflow { + raster[y_val * WIDTH + x] = colour; + } + } else { + draw_missing(raster, x); + } + } +} + +fn draw_missing(raster: &mut [u8], x: usize) { + for y in 0..HEIGHT { + // Makes a dashed line, alternating between the MISSING colour + // and nothing every four pixels + if (y / 4) % 2 == 0 { + raster[y * WIDTH + x] = MISSING; + } + } +} diff --git a/awake/src/main.rs b/awake/src/main.rs new file mode 100755 index 0000000..3501e1f --- /dev/null +++ b/awake/src/main.rs @@ -0,0 +1,478 @@ +mod atom; +mod db; +mod error; +mod fs; +mod gatherer; +mod griph; +mod markup; +mod settings; +mod templated; +mod timeparse; +mod util; + +use std::{ + fs::File, + io::{BufRead, BufReader, Write}, + os::unix::fs::MetadataExt, + str::FromStr, + sync::{ + atomic::{AtomicU8, AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; + +use ::time::OffsetDateTime; +use axum::{ + body::Body, + extract::{Path, State}, + http::{header, StatusCode}, + response::Response, + routing::get, + Extension, Router, +}; +use bempline::{variables, Document, Options}; +use camino::Utf8PathBuf; +use confindent::{Confindent, Node}; +use db::Database; +pub use error::RuntimeError; +use fs::Filesystem; +use gatherer::Gatherer; +use settings::Settings; +use templated::Frontmatter; +use tokio_util::io::ReaderStream; +use tracing_subscriber::{fmt::time, prelude::*, EnvFilter}; +use util::{Referer, RemoteIp, SessionId}; + +use crate::{ + fs::{PathResolution, Webpath}, + markup::Markup, + templated::Templated, +}; + +#[derive(Clone)] +pub struct AwakeState { + pub do_statistics: bool, + pub database: Arc, + pub cache_path: Utf8PathBuf, + /// kbps + pub netinfo_upper_bound: Arc, + /// whole digit % cpu usage + pub cpuinfo_upper_bound: Arc, +} + +#[tokio::main] +async fn main() { + match std::env::args().nth(1).as_deref() { + Some("atomizer") => atom::main(), + /* fallthrough*/ + Some("serve") => (), + _ => (), + } + + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with( + EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(), + ) + .init(); + + let conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap(); + let webroot: Utf8PathBuf = conf.child_parse("Webroot").unwrap(); + let templates = conf.child_value("Templates").unwrap(); + let hostname = conf.child_owned("Hostname").unwrap(); + let dbpath = conf.child_owned("Database").unwrap(); + let cache = conf.child_owned("Cache").unwrap(); + let statistics = match conf.child_value("Statistics") { + None | Some("true") | Some("yes") | Some("collect") => true, + _ => false, + }; + + let database = Database::new(dbpath.into()); + database.create_tables(); + + let fs = Filesystem::new(&webroot); + + let state = AwakeState { + do_statistics: statistics, + database: Arc::new(database), + cache_path: cache.into(), + netinfo_upper_bound: Arc::new(AtomicUsize::new(256)), + cpuinfo_upper_bound: Arc::new(AtomicUsize::new(100)), + }; + + match std::env::args().nth(1).as_deref() { + Some("graph") => { + gatherer::make_mem_graph(&state); + gatherer::make_cpu_graph(&state); + gatherer::make_net_graph(&state); + } + _ => (), + } + + let mut gatherer = Gatherer::new(state.clone()); + gatherer.start(); + + let settings = Settings { + template_dir: Utf8PathBuf::from(webroot.join(templates)) + .canonicalize_utf8() + .unwrap(), + hostname, + }; + + let app = Router::new() + .route("/", get(index_handler)) + .route("/api/stats/:name", get(stats)) + .route("/*path", get(handler)) + .layer(Extension(fs)) + .layer(Extension(settings)) + .with_state(state); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:2560").await.unwrap(); + axum::serve(listener, app).await.unwrap() +} + +async fn index_handler( + state: State, + fse: Extension, + se: Extension, + sid: SessionId, + rfr: Option, +) -> Response { + handler(state, fse, se, sid, rfr, Path(String::from("/"))).await +} + +async fn handler( + State(state): State, + Extension(fs): Extension, + Extension(settings): Extension, + sid: SessionId, + rfr: Option, + Path(path): Path, +) -> Response { + match falible_handler(state, fs, settings, sid, rfr, path).await { + Ok(resp) => resp, + Err(RuntimeError::NotFound { source: _, path }) => { + tracing::warn!("[{sid}] 404 on {path}"); + + Response::builder() + .body(Body::from(format!("the path was not found: {path}"))) + .unwrap() + } + Err(re) => Response::builder() + .body(Body::from(re.to_string())) + .unwrap(), + } +} + +async fn falible_handler( + state: AwakeState, + fs: Filesystem, + settings: Settings, + sid: SessionId, + rfr: Option, + path: String, +) -> Result { + tracing::debug!("webpath = {path}"); + + let webpath: Webpath = path.parse()?; + let resolve = fs.resolve(&webpath)?; + + if !webpath.is_dir() && resolve.is_dirfile { + return Ok(redirect(webpath.as_dir())); + } + + let ext = resolve.filepath.extension().unwrap_or_default(); + + tracing::trace!("[{sid}] referer = {rfr:?}"); + tracing::trace!("[{sid}] webpath = {webpath}"); + + if ext != "html" { + // Logging a non-html asset without a referrer. Let's pretend like + // that means it's the primary resource. + if rfr.is_none() { + tracing::info!("[{sid}] serving {webpath}"); + } + + send_file(resolve.filepath).await + } else { + match rfr { + None => { + tracing::info!("[{sid}] serving {webpath}"); + } + Some(referer) => { + tracing::info!("[{sid}] (refer {referer}) serving {webpath}"); + } + } + + let content = Filesystem::read_to_string(&resolve.filepath).await?; + let result = Templated::from_str(&content); + + match result { + Ok(templated) => send_template(templated, resolve, webpath, state, settings, sid).await, + Err(e) => { + tracing::warn!("error sending template {e}"); + + Ok(Response::builder() + .header(header::CONTENT_TYPE, "text/html") + .body(Body::from(content)) + .unwrap()) + } + } + } +} + +fn redirect>(redirection: S) -> Response { + let location = redirection.into(); + tracing::info!("redirect to {location}"); + Response::builder() + .status(StatusCode::TEMPORARY_REDIRECT) + .header(header::LOCATION, &location) + .body(Body::new(format!("redirecting to {location}"))) + .unwrap() +} + +// 20 megabytes +const STREAM_AFTER: u64 = 20 * 1024 * 1024; + +async fn send_file(filepath: Utf8PathBuf) -> Result { + tracing::debug!(target: "send_file", filepath = ?filepath); + let ext = filepath.extension().unwrap_or_default(); + let stem = filepath.file_stem().unwrap_or_default(); + + let mime = match ext { + // Text + "css" => "text/css", + "html" => "text/html", + "js" => "text/javascript", + "txt" => "text/plain", + "xml" if stem.ends_with("atom") => "application/atom+xml", + "xml" => "application/xml", + + // Multimedia + "gif" => "image/gif", + "jpg" | "jpeg" => "image/jpeg", + "mp4" => "video/mp4", + "png" => "image/png", + _ => "", + }; + tracing::debug!(target: "send_file", mime = ?mime); + + let mut response = Response::builder(); + if !mime.is_empty() { + response = response.header(header::CONTENT_TYPE, mime); + } + + let metadata = Filesystem::metadata(&filepath)?; + if metadata.size() > STREAM_AFTER { + tracing::debug!("large file, streaming to client"); + + let file = Filesystem::open(filepath).await?; + let stream = ReaderStream::new(file); + Ok(response.body(Body::from_stream(stream)).unwrap()) + } else { + tracing::debug!("small file, sending entirely at once"); + + let content = Filesystem::read(filepath).await?; + Ok(response.body(Body::from(content)).unwrap()) + } +} + +async fn send_template( + templated: Templated, + resolve: PathResolution, + webpath: Webpath, + state: AwakeState, + settings: Settings, + sid: SessionId, +) -> Result { + tracing::trace!("[{sid}] sending template"); + let template_stem = templated.frontmatter.get("template").expect("no template"); + let template_name = Utf8PathBuf::from(format!("{template_stem}.html")); + let template_path = settings.template_dir.join(template_name); + + let filename = resolve + .filepath + .file_name() + .expect("template has no filename"); + + let mut template = Document::from_file( + template_path, + Options::default().include_path(bempline::options::IncludeMethod::Path( + settings.template_dir.as_std_path().to_owned(), + )), + ) + .unwrap(); + + let title = templated.frontmatter.get("title").unwrap_or(filename); + + template.set("title", title); + + tracing::trace!("doing opengraph stuff!"); + if let Some(og_description) = templated.frontmatter.get("description") { + let og_title = title; + let og_url = format!("https://{}{}", &settings.hostname, webpath); + + if let Some(art_relpath) = templated.frontmatter.get("art") { + let serving_dir = Utf8PathBuf::from(webpath.first_dir()); + let art_path = serving_dir.join(art_relpath); + + let og_image_alt = match templated.frontmatter.get("art_alt") { + Some(alt) => alt, + None => { + tracing::warn!("[{sid}] {} has art but no alt", resolve.filepath); + "" + } + }; + + let og_image = format!("https://{}/{}", &settings.hostname, art_path); + + variables!(template, og_image, og_image_alt); + } + + let og_site_name = &settings.hostname; + + variables!(template, og_title, og_url, og_description, og_site_name); + } + + tracing::trace!("stylin'!"); + // styles the templated stuff wants + let style_pattern = template.get_pattern("styles").unwrap(); + for style in templated.frontmatter.get_many("style") { + let mut pat = style_pattern.clone(); + pat.set("style", style); + template.set_pattern(pat); + } + + // path to the file for navigation + let mut path: Vec<&str> = webpath.webcanon.iter().collect(); + // we don't want the directory/filename itself + path.pop(); + + if let Some(path_pattern) = template.get_pattern("path") { + let offset = match templated + .frontmatter + .get("path-offset") + .map(|raw| raw.parse::()) + { + Some(Ok(offset)) => offset, + None => 0, + Some(Err(_)) => { + tracing::error!( + "[{sid}] path-offset in template {} is not an integer", + resolve.filepath + ); + + 0 + } + }; + + for _ in 0..offset { + path.pop(); + } + + let mut link = Utf8PathBuf::from("/"); + + let mut pat = path_pattern.clone(); + pat.set("path_link", "/"); + pat.set("path_name", "home"); + template.set_pattern(pat); + + for part in path { + link.push(part); + + let mut pat = path_pattern.clone(); + pat.set("path_link", &link); + pat.set("path_name", part); + template.set_pattern(pat); + } + } + + tracing::trace!("starting published block"); + 'published: { + if let Some(mut published_pattern) = template.get_pattern("published") { + let publish_date_result = templated + .frontmatter + .get("published") + .map(|ts| timeparse::parse(ts)); + + match publish_date_result { + None => break 'published, + Some(Err(_e)) => { + tracing::warn!("template {resolve} has malformed `published` frontmatter"); + break 'published; + } + Some(Ok(datetime)) => { + let published_human = timeparse::format_long(&datetime); + let published_machine = timeparse::iso8601(&datetime); + + variables!(published_pattern, published_human, published_machine); + template.set_pattern(published_pattern); + } + } + } + } + tracing::trace!("finished published block"); + + // insert the page content itself + let markedup = markup::process(&templated.content); + markedup.fill_document(&mut template); + + if templated.frontmatter.get("use-template").is_some() { + let main = template_content(state, &templated.frontmatter, markedup); + template.set("main", main); + } else { + template.set("main", markedup.into_content()); + } + + Ok(Response::builder() + .header(header::CONTENT_TYPE, "text/html") + .body(Body::from(template.compile())) + .unwrap()) +} + +fn template_content(state: AwakeState, frontmatter: &Frontmatter, marked: Markup) -> String { + let mut doc = match marked.into_document() { + Ok(doc) => doc, + Err(content) => return content, + }; + + if frontmatter.get("system-stats").is_some() { + let mem = state.database.get_last_host_meminfo(); + + doc.set("stats.mem.total", mem.total_kb / 1000); + doc.set("stats.mem.usage", mem.usage() / 1000); + + let netinfo_upper = state.netinfo_upper_bound.load(Ordering::Relaxed); + doc.set("stats.net.max_bound", netinfo_upper); + + let cpuinfo_upper = state.cpuinfo_upper_bound.load(Ordering::Relaxed); + doc.set("stats.cpu.max_bound", cpuinfo_upper); + } + + doc.compile() +} + +async fn stats(State(state): State, Path(name): Path) -> Response { + const NAMES: &[&'static str] = &[ + "current_hostmeminfo.gif", + "current_hostnetinfo.gif", + "current_hostcpuinfo.gif", + ]; + + if NAMES.contains(&name.as_str()) { + let path = state.cache_path.join(name); + match send_file(path).await { + Ok(resp) => resp, + Err(e) => Response::builder().body(Body::from(e.to_string())).unwrap(), + } + } else { + tracing::debug!("invalid stat requested: {name}"); + + Response::builder() + .body(Body::from("that stat is not real")) + .unwrap() + } +} diff --git a/awake/src/markup.rs b/awake/src/markup.rs new file mode 100755 index 0000000..bde28ee --- /dev/null +++ b/awake/src/markup.rs @@ -0,0 +1,409 @@ +use std::{borrow::Cow, ops::Deref}; + +use bempline::{Document, Options}; + +struct State { + active_id: Option, + active_fill: Option, + paragraphs: bool, + + processed: String, + current: String, + fills: Vec, + escaped_html: bool, + last_blank: bool, +} + +impl Default for State { + fn default() -> Self { + Self { + active_id: None, + active_fill: None, + paragraphs: true, + + processed: String::new(), + current: String::new(), + fills: vec![], + escaped_html: false, + last_blank: true, + } + } +} + +impl State { + /// Get an opening paragraph tag with any attributes currently set. + fn get_open_paragraph(&mut self) -> String { + match self.active_id.take() { + None => String::from("

"), + Some(id) => format!(r#"

"#), + } + } + + fn paragraph(&mut self) { + let current = self.take_current(); + let content = self.make_paragraph(current); + + if let Some(fill) = self.active_fill.as_mut() { + // linebreak if there is already text pushed to fill.content + if !fill.content.is_empty() { + fill.content.push('\n'); + } + + fill.content.push_str(&content); + } else { + // linebreak if there is already text pushed to final + if !self.processed.is_empty() { + self.processed.push('\n'); + } + + self.processed.push_str(&content); + } + } + + fn make_paragraph(&mut self, content: String) -> String { + if self.should_paragraph_string(&content) { + let open = self.get_open_paragraph(); + format!("{open}\n{}\n

", content) + } else { + content + } + } + + fn should_paragraph_string(&self, string: &str) -> bool { + // wrap paragraphs if all of these are true: + // - we're supposed to be wrapping paragraphs + // - either of these is true: + // - the line does not start with < + // OR + // - the line starts with < AND it's been escaped + self.paragraphs + && (!string.starts_with('<') || (string.starts_with('<') && self.escaped_html)) + } + + pub fn process_line(&mut self, line: &str) { + if self.parse_command(line) { + // Don't set last_blank; a command does not break up a text block + return; + } + + if !line.is_empty() { + self.push_current(line); + + self.last_blank = false; + } else { + if !self.paragraphs { + // It's a blank line, but we're not doing paragraphs, + // so leave it in the document + self.current.push('\n'); + self.last_blank = false; + return; + } + + // line is empty, close the textblock + self.finish_current(); + } + } + + pub fn done(mut self) -> Markup { + self.finish_current(); + self.finish_active_fill(); + + Markup { + content: self.processed, + fill: self.fills, + } + } + + fn escape_line<'a>(&mut self, line: &'a str) -> &'a str { + if let Some(strip) = line.strip_prefix('\\') { + match strip.chars().next() { + Some('[') => strip, + Some('<') => { + if self.last_blank { + self.escaped_html = true; + } + + strip + } + Some('\\') => strip, + _ => line, + } + } else { + line + } + } + + /// Possibly parses a line as a command and mutates internal state. + /// # Returns + /// true if the line was a command, false otherwise. + fn parse_command(&mut self, line: &str) -> bool { + match line.strip_prefix('[') { + Some(line) => match line.strip_suffix(']') { + Some(cmd) => self.run_command(cmd), + None => false, + }, + None => false, + } + } + + fn run_command(&mut self, cmd: &str) -> bool { + match cmd.trim() { + "@paragraphs off" => { + self.finish_current(); + self.paragraphs = false; + true + } + "@paragraphs on" => { + self.finish_current(); + self.paragraphs = true; + true + } + "@fill-end" => { + self.finish_current(); + self.finish_active_fill(); + true + } + template_key if cmd.starts_with("@fill ") => { + self.finish_current(); + + let key = template_key.strip_prefix("@fill ").unwrap().to_owned(); + self.active_fill = Some(FillComand { + paragraphs: self.paragraphs, + key, + content: String::new(), + }); + self.paragraphs = false; + + true + } + annotation if cmd.starts_with('#') => { + self.active_id = Some(annotation[1..].to_owned()); + true + } + _ => false, + } + } + + fn take_current(&mut self) -> String { + self.current.split_off(0) + } + + fn push_current(&mut self, text: &str) { + if !self.current.is_empty() { + self.current.push('\n'); + } + + let escaped = self.escape_line(text); + self.current.push_str(escaped); + } + + fn finish_current(&mut self) { + if !self.current.is_empty() { + // Calling paragraph clears self.current + self.paragraph(); + + // reset block dependant state + self.last_blank = true; + self.escaped_html = false; + } + } + + fn finish_active_fill(&mut self) { + if let Some(fill) = self.active_fill.take() { + self.fills.push(Fill { + key: fill.key, + content: fill.content, + }); + + self.paragraphs = fill.paragraphs; + } + } +} + +pub struct Markup { + content: String, + fill: Vec, +} + +impl Markup { + pub fn fill(&self) -> &[Fill] { + &self.fill + } + + pub fn content(&self) -> &str { + &self.content + } + + pub fn into_content(self) -> String { + self.content + } + + pub fn into_document(self) -> Result { + let Ok(mut doc) = Document::from_str(&self.content, Options::default()) else { + return Err(self.content); + }; + + self.fill_document(&mut doc); + + Ok(doc) + } + + pub fn fill_document(&self, doc: &mut Document) { + for fill in &self.fill { + doc.set(&fill.key, &fill.content); + } + } +} + +impl Deref for Markup { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.content + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Fill { + pub key: String, + pub content: String, +} + +pub fn process(raw: &str) -> Markup { + tracing::trace!("processing!"); + let mut state = State::default(); + + for line in raw.lines() { + println!("raw line: '{line}'"); + state.process_line(line) + } + + state.done() +} + +struct FillComand { + /// Storage for the previous paragraphs variable, as @fill unsets it + paragraphs: bool, + + key: String, + content: String, +} + +#[cfg(test)] +mod test { + use crate::markup::{process, Fill}; + + #[test] + fn parses_no_commands() { + let blk1 = "line one\nline two"; + let blk2 = "block two"; + + assert_eq!(*process(blk1), format!("

\n{blk1}\n

")); + + let tst = format!("{blk1}\n\n{blk2}"); + assert_eq!( + *process(&tst), + format!("

\n{blk1}\n

\n

\n{blk2}\n

") + ) + } + + #[test] + fn parses_paragraph_off() { + let str = "[@paragraphs off]\none two\n\nthree\nfour"; + assert_eq!(*process(str), "one two\n\nthree\nfour") + } + + #[test] + fn parses_adds_annotation() { + let str = "[#greeting]\nHello!"; + assert_eq!(*process(str), "

\nHello!\n

") + } + + #[test] + fn doesnt_wrap_html() { + let str = "hello!\n\nhi, how are you?"; + assert_eq!(*process(str), "

\nhello!\n

\nhi, how are you?") + } + + #[test] + fn correctly_escapes() { + let str = "\\[@paragraph on]\n\\Hello!\n\\\\Goodbye!"; + let correct = "

\n[@paragraph on]\nHello!\n\\Goodbye!\n

"; + assert_eq!(*process(str), correct) + } + + #[test] + fn wraps_escaped_html() { + let str = "\\test"; + let correct = "

\ntest\n

"; + assert_eq!(*process(str), correct) + } + + #[test] + fn parses_extracts_fill() { + let str = "[@fill key]\nfilled stuff!\n[@fill-end]\nmeow"; + let correct = "

\nmeow\n

"; + let marked = process(str); + assert_eq!( + marked.fill, + vec![Fill { + key: "key".into(), + content: "filled stuff!".into() + }] + ); + assert_eq!(marked.content, correct) + } + + const BASE: &str = "test/markup"; + fn test_files(test: &str) { + let input_path = format!("{BASE}/{test}/input.html"); + let output_path = format!("{BASE}/{test}/output.html"); + + let input = std::fs::read_to_string(input_path).unwrap(); + let output = std::fs::read_to_string(output_path).unwrap(); + + assert_eq!(*process(&input), output) + } + + #[test] + fn parses_onoff() { + test_files("paragraph toggle") + } +} + +#[cfg(test)] +mod document_test { + use crate::markup::process; + + const BASE: &str = "test/markup"; + fn test_files(test: &str) { + let input_path = format!("{BASE}/{test}/input.html"); + let output_path = format!("{BASE}/{test}/output.html"); + + let input = std::fs::read_to_string(input_path).unwrap(); + let output = std::fs::read_to_string(output_path).unwrap(); + + let actual = process(&input).into_document().unwrap().compile(); + + assert_eq!(actual, output) + } + + #[test] + fn inserts_fill() { + let input = "[@fill main]\nwords!\n[@fill-end]\n{main}"; + let expected = "

\nwords!\n

"; + let markup = process(input); + let doc = markup.into_document().unwrap().compile(); + assert_eq!(doc, expected) + } + + #[test] + fn fill_restores_paragraphs() { + test_files("fill paragraphs"); + } + + #[test] + fn parses_complicated_fill() { + test_files("complicated fill"); + } +} diff --git a/awake/src/settings.rs b/awake/src/settings.rs new file mode 100755 index 0000000..06abc18 --- /dev/null +++ b/awake/src/settings.rs @@ -0,0 +1,7 @@ +use camino::Utf8PathBuf; + +#[derive(Clone, Debug)] +pub struct Settings { + pub template_dir: Utf8PathBuf, + pub hostname: String, +} diff --git a/awake/src/templated.rs b/awake/src/templated.rs new file mode 100755 index 0000000..4598ecc --- /dev/null +++ b/awake/src/templated.rs @@ -0,0 +1,130 @@ +use std::str::FromStr; + +/// The content part of a file that fills in a template. +pub struct Templated { + pub frontmatter: Frontmatter, + pub content: String, +} + +impl FromStr for Templated { + type Err = TemplateError; + + fn from_str(raw: &str) -> Result { + tracing::trace!("parsing Templated"); + + let (front, content) = match raw.strip_prefix("---\n") { + None => return Err(TemplateError::MissingFrontmatter), + Some(no_start) => match no_start.split_once("\n---\n") { + None => return Err(TemplateError::MissingFrontmatter), + Some((front, content)) => (front, content), + }, + }; + + tracing::trace!("isolated frontmatter"); + + Ok(Self { + frontmatter: front.parse()?, + content: content.to_owned(), + }) + } +} + +pub struct Frontmatter { + entries: Vec<(String, String)>, +} + +impl FromStr for Frontmatter { + type Err = TemplateError; + + fn from_str(raw: &str) -> Result { + let mut entries = vec![]; + + for line in raw.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + // Skip over blank lines and comments + continue; + } + + let (key, value) = match trimmed.split_once('=') { + None => { + return Err(TemplateError::MalformedFrontmatterEntry { + line: trimmed.to_owned(), + }); + } + Some(tup) => tup, + }; + + entries.push((key.trim().to_owned(), value.trim().to_owned())); + tracing::trace!("pushed frontmatter entry {} successfully", key.trim()); + } + + Ok(Self { entries }) + } +} + +impl Frontmatter { + pub fn get(&self, key: &str) -> Option<&str> { + self.entries + .iter() + .find(|(k, _)| k == key) + .map(|(_, v)| v.as_str()) + } + + pub fn get_many(&self, key: &str) -> Vec<&str> { + // this could probably be a filter_map() but the explicit filter and + // then map seems cleaner + self.entries + .iter() + .filter(|(k, _)| k == key) + .map(|(_, v)| v.as_str()) + .collect() + } +} + +#[derive(Debug, snafu::Snafu)] +pub enum TemplateError { + #[snafu(display("file is missing the frontmatter"))] + MissingFrontmatter, + #[snafu(display("the frontmatter entry is malformed: {line}"))] + MalformedFrontmatterEntry { line: String }, +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use crate::templated::{Frontmatter, Templated}; + + #[test] + fn frontmatter_parse() { + let frntmtr = "key=value\nkey2=value2"; + assert!(Frontmatter::from_str(frntmtr).is_ok()) + } + + #[test] + fn frontmatter_fails_on_invalid_line() { + let front = "key1=value1\nincorrect line"; + assert!(Frontmatter::from_str(front).is_err()) + } + + #[test] + fn templated_parse() { + let tempalted = "---\ntitle=Title!\n---\nContent line!"; + assert!(Templated::from_str(tempalted).is_ok()) + } + + #[test] + fn templated_doesnt_eat_frontmatter_line() { + let templated = "---\ntitle=Title---\nContent"; + assert!(Templated::from_str(templated).is_err()); + } + + #[test] + fn templated_parses_correct() { + let raw = "---\ntitle=Title!\n---\n

Paragraph!

"; + let tmpl = Templated::from_str(raw).unwrap(); + assert_eq!(tmpl.frontmatter.get("title").unwrap(), "Title!"); + assert_eq!(tmpl.content, "

Paragraph!

") + } +} diff --git a/awake/src/timeparse.rs b/awake/src/timeparse.rs new file mode 100755 index 0000000..fe8ca60 --- /dev/null +++ b/awake/src/timeparse.rs @@ -0,0 +1,216 @@ +use time::{ + error::{Parse, TryFromParsed}, + format_description::{well_known, FormatItem}, + macros::{format_description, offset, time}, + Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday, +}; + +const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]"); + +const DATE: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); +const TIME_24HOUR: &[FormatItem<'_>] = format_description!("[hour padding:zero]:[minute]"); +const TIME_12HOUR: &[FormatItem<'_>] = + format_description!("[hour padding:none]:[minute][period case:lower case_sensitive:false]"); + +#[allow(unused)] +const OFFSET: &[FormatItem<'_>] = + format_description!("[offset_hour sign:mandatory]:[offset_minute]"); + +const CST: UtcOffset = offset!(-06:00); +const CDT: UtcOffset = offset!(-05:00); + +// Ripped from infica with the month of Sol removed. I just want the 3 letter months +/// Capitalized months (long, short) and lowercase months (long, short). +// it seems useful to have the lowercase here so we don't have to always call +// to_lowercase +const MONTHS: [[&str; 4]; 12] = [ + ["January", "Jan", "january", "jan"], + ["February", "Feb", "february", "feb"], + ["March", "Mar", "march", "mar"], + ["April", "Apr", "april", "apr"], + ["May", "May", "may", "may"], + ["June", "Jun", "june", "jun"], + ["July", "Jul", "july", "jul"], + ["August", "Aug", "august", "aug"], + ["September", "Sep", "september", "sep"], + ["October", "Oct", "october", "oct"], + ["November", "Nov", "november", "nov"], + ["December", "Dec", "december", "dec"], +]; + +/// Offset for the united states in Central Time. Accounts for DST +/// DST starts the 2nd sunday in March and ends the 1st sunday in November. +/// https://www.nist.gov/pml/time-and-frequency-division/popular-links/daylight-saving-time-dst +fn us_dst_central_offset(datetime: PrimitiveDateTime) -> UtcOffset { + let second_sunday_march = { + let mut seen_sunday = false; + let mut curr = Date::from_calendar_date(datetime.year(), time::Month::March, 1).unwrap(); + + loop { + if curr.weekday() == Weekday::Sunday { + if seen_sunday { + break PrimitiveDateTime::new(curr, time!(02:00 AM)); + } else { + seen_sunday = true; + } + } + + curr = curr.next_day().unwrap(); + } + }; + + let first_sunday_november = { + let mut curr = Date::from_calendar_date(datetime.year(), time::Month::November, 1).unwrap(); + + loop { + if curr.weekday() == Weekday::Sunday { + break PrimitiveDateTime::new(curr, time!(02:00 AM)); + } else { + curr = curr.next_day().unwrap(); + } + } + }; + + if datetime >= second_sunday_march && datetime < first_sunday_november { + CDT + } else { + CST + } +} + +fn parse_offset(raw: &str) -> UtcOffset { + match raw.to_ascii_uppercase().as_str() { + "CST" => CST, + "CDT" => CDT, + _ => unimplemented!(), + } +} + +fn parse_time(raw: &str) -> Result { + match Time::parse(raw, TIME_24HOUR) { + Err(_e) => Time::parse(raw, TIME_12HOUR), + Ok(t) => Ok(t), + } +} + +fn parse_date(raw: &str) -> Result { + Date::parse(raw, DATE) +} + +/// Parses an OffsetDateTime from any of the following formats: +/// - date only: 2024-04-13 +/// - date and time: 2024-04-13 2:56 +/// - dat, time, and offset: 2024-04-13 2:56 CDT +/// +/// If a time is not procided, noon (12:00) is assumed. +/// +/// If an offset is not provided it will be assumed to be US Central Time and +/// it will correct for daylight savings. +pub fn parse(raw: &str) -> Result { + let mut splits = raw.trim().split(' '); + + let date = match splits.next() { + None => return Err(Parse::TryFromParsed(TryFromParsed::InsufficientInformation)), + Some(raw) => parse_date(raw)?, + }; + + let time = match splits.next() { + None => time!(12:00:00), + Some(raw) => parse_time(raw)?, + }; + + let calculated_offset = us_dst_central_offset(PrimitiveDateTime::new(date, time)); + let offset = match splits.next() { + None => calculated_offset, + Some(raw) => { + let offset = parse_offset(raw); + if offset != calculated_offset { + //FIXME: gen 2024-12; warn here but format it rightly. + () + } + + offset + } + }; + + Ok(OffsetDateTime::new_in_offset(date, time, offset)) +} + +pub fn format_long(datetime: &OffsetDateTime) -> String { + let year = datetime.year(); + let month = MONTHS[datetime.month() as usize - 1][1]; + let day = datetime.day(); + let weekday = datetime.weekday(); + + format!("{weekday} {day}, {month} {year}") +} + +pub fn iso8601(datetime: &OffsetDateTime) -> String { + datetime + .format(&well_known::Iso8601::DATE_TIME_OFFSET) + .unwrap() +} + +#[cfg(test)] +mod test { + use time::{ + macros::{datetime, offset}, + OffsetDateTime, PrimitiveDateTime, + }; + + use crate::timeparse::{parse_offset, us_dst_central_offset, CDT, CST, FMT}; + + #[test] + fn calculates_cst_cdt_correctly() { + fn daylight(raw: &str) { + let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); + + if us_dst_central_offset(date) != CDT { + panic!("CDT failed on {raw}") + } + } + + fn standard(raw: &str) { + let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); + + if us_dst_central_offset(date) != CST { + panic!("CST failed on {raw}") + } + } + + // make sure CST and CDT are what we expect + assert_eq!(CST, offset!(-06:00)); + assert_eq!(CDT, offset!(-05:00)); + + daylight("2024-04-13 02:56"); + daylight("2024-03-10 14:00"); + daylight("2024-03-10 02:00"); + + standard("2024-12-01 00:00"); + standard("2024-11-03 14:00"); + standard("2024-11-03 02:00"); + + // other years + daylight("2023-03-12 12:00"); // DST start Mar 12, end Nov 5 + standard("2023-03-11 23:00"); + + standard("2023-11-05 12:00"); + daylight("2023-11-04 23:00"); + } + + #[test] + fn paress_timezone_code() { + assert_eq!(parse_offset("CST"), CST); + assert_eq!(parse_offset("CDT"), CDT); + } + + fn test_parse(raw: &str, expected: OffsetDateTime) { + assert_eq!(super::parse(raw).unwrap(), expected) + } + + #[test] + fn parse_date_only() { + test_parse("2024-04-13", datetime!(2024-04-13 12:00 -05:00)); + test_parse("2024-01-01", datetime!(2024-01-01 12:00 -06:00)); + } +} diff --git a/awake/src/util.rs b/awake/src/util.rs new file mode 100755 index 0000000..a3cc064 --- /dev/null +++ b/awake/src/util.rs @@ -0,0 +1,137 @@ +use core::fmt; +use std::ops::Deref; + +use axum::{ + async_trait, + extract::FromRequestParts, + http::{header, request::Parts, HeaderName, StatusCode}, + RequestPartsExt, +}; +use sha2::{Digest, Sha256}; +use time::{format_description::FormatItem, macros::format_description, OffsetDateTime}; + +#[derive(Debug, Clone)] +pub struct RemoteIp(String); + +#[async_trait] +impl FromRequestParts for RemoteIp +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + if let Some(forwarded_for) = parts + .headers + .get(HeaderName::from_static("x-forwarded-for")) + { + match forwarded_for.to_str() { + Err(_e) => Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")), + Ok(remoteip) => Ok(Self(remoteip.to_owned())), + } + } else { + Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")) + } + } +} + +impl Deref for RemoteIp { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); + +#[derive(Debug, Copy, Clone)] +pub struct SessionId([u8; 4]); + +#[async_trait] +impl FromRequestParts for SessionId +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let remote = parts + .extract::() + .await + .map(|rip| rip.0) + .unwrap_or(String::from("127.0.0.1")); + + let agent = parts + .headers + .get(header::USER_AGENT) + .and_then(|ua| ua.to_str().ok()) + .unwrap_or("unknown"); + + let date = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + + let string = format!("{remote} - {agent} - {}", date.format(FMT).unwrap()); + + let mut hasher = Sha256::new(); + hasher.update(string.as_bytes()); + let hash = hasher.finalize(); + let slc: [u8; 4] = hash.as_slice()[0..4].try_into().unwrap(); + + Ok(Self(slc)) + } +} + +impl fmt::Display for SessionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.0.as_slice() { + write!(f, "{byte:02X}")?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct Referer(String); + +#[async_trait] +impl FromRequestParts for Referer +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + if let Some(refer) = parts.headers.get(header::REFERER) { + match refer.to_str() { + Err(_e) => Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")), + Ok(refer) => match url::Url::parse(refer) { + Err(_e) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "failed to parse referer url", + )), + Ok(url) => match url.host_str() { + None => Err((StatusCode::INTERNAL_SERVER_ERROR, "referer url had no host")), + Some(str) => Ok(Referer(str.to_owned())), + }, + }, + } + } else { + Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")) + } + } +} + +impl Deref for Referer { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for Referer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/justfile b/justfile index 3ab2744..da205b8 100644 --- a/justfile +++ b/justfile @@ -6,9 +6,12 @@ prod_db := "/var/awake/database.sqlite" get-db: rsync -aP {{prod}}:{{prod_db}} . +run: + cargo run --release -- serve dev.conf + graph: get-db - cargo run --release -- graph dev.conf + cargo run --release --bin awake -- graph dev.conf deploy: - cargo build --release --target x86_64-unknown-linux-gnu + cargo build --release --bin awake --target x86_64-unknown-linux-gnu rsync -aP target/x86_64-unknown-linux-gnu/release/awake {{prod}}:{{prod_bin}} \ No newline at end of file diff --git a/src/atom/mod.rs b/src/atom/mod.rs deleted file mode 100644 index 2020163..0000000 --- a/src/atom/mod.rs +++ /dev/null @@ -1,154 +0,0 @@ -mod urn; - -use std::str::FromStr; - -use bempline::{variables, Document, Options}; -use camino::{Utf8Path, Utf8PathBuf}; -use confindent::{let_get, Confindent, Entry, Node}; -use time::{format_description::well_known, OffsetDateTime}; -use urn::Urn; - -use crate::{markup, templated::Templated, timeparse}; - -pub fn main() -> ! { - let awake_conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap(); - - let root: Utf8PathBuf = awake_conf.child_parse("Webroot").unwrap(); - - // Grab the atom config and then grab the atom template - let atom_conf_path = std::env::args().nth(3).unwrap_or(String::from("atom.conf")); - let mut atom_conf = Confindent::from_file(&atom_conf_path).unwrap(); - let atom = Document::from_file( - root.clone() - .join(atom_conf.child_value("Template").unwrap()) - .canonicalize_utf8() - .unwrap(), - Options::default(), - ) - .unwrap(); - - let mut changed = false; - // Go through every feed and create the entries - let feeds = atom_conf.children_mut("Feed"); - for feed in feeds { - make_feed(feed, &mut changed, &root, atom.clone()) - } - - // If the atom has changed, probably because we created an ID - // write it to disk. - if changed { - std::fs::write(atom_conf_path, atom_conf.to_string()); - } - - std::process::exit(0); -} - -pub struct AtomFeed { - /// Where is the atom located in the webroot. (relative to webroot) - relpath: Utf8PathBuf, - /// What unique ID does this Feed have? - id: String, - - title: String, - subtitle: String, - updated: OffsetDateTime, - author_name: String, - author_email: String, - - entries: Vec, -} - -pub struct AtomEntry { - /// What file does this entry direct to? This file will be opened to inspect - /// the frontmatter for information to fill out the entry with. It will also - /// be used to create the link to the entry. - relpath: Utf8PathBuf, - /// What unique ID does this Entry have? - id: String, - - title: String, - published: OffsetDateTime, - updated: OffsetDateTime, -} - -/// Parses all the options of a feed and generates the build job -pub fn make_feed(feed: &mut Entry, changed: &mut bool, root: &Utf8Path, mut atom: Document) { - let_get!( - feed, - relpath: Utf8PathBuf = "Output", - clone id = "Id", - clone title = "Title", - clone subtitle = "Subtitle", - clone updated = "Updated", - ); - - //FIXME: gen- We need to backresolve the feed URL - let feed_link = ""; - let author_name = feed.get("Author/Name").unwrap().to_owned(); - let author_email = feed.get("Author/Email").unwrap().to_owned(); - - let mut atomfeed = AtomFeed { - relpath, - id, - title, - subtitle, - updated: timeparse::parse(&updated).unwrap(), - author_name, - author_email, - entries: vec![], - }; - - let entries = feed.children_mut("Entry"); - for entry in entries { - if !entry.has_child("Id") { - //TODO: gen- Read NID from conf? - let id = Urn::new_random("dreamy"); - entry.push_entry(("Id", id)); - *changed = true; - } - - atomfeed.entries.push(make_entry(entry, &root)); - } -} - -fn make_entry>(entry: &Entry, webroot: P) -> AtomEntry { - let relpath: Utf8PathBuf = entry.parse().unwrap(); - let path = webroot.as_ref().join(&relpath); - - let entry_content = match std::fs::read_to_string(&path) { - Ok(ok) => ok, - Err(e) => { - panic!("failed to get file at {path}: {e}"); - } - }; - let entry_templated = Templated::from_str(&entry_content).unwrap(); - - let conf_or_frontmatter = |conf_key: &str, front_key: &str| -> &str { - entry - .child_value(conf_key) - .unwrap_or_else(|| entry_templated.frontmatter.get(front_key).unwrap()) - }; - - let title = conf_or_frontmatter("Title", "title").to_owned(); - let id = entry.child_value("Id").unwrap().to_owned(); - - let entry_published_raw = conf_or_frontmatter("Published", "published"); - - let updated = timeparse::parse(entry.child_value("Updated").unwrap_or_else(|| { - entry_templated - .frontmatter - .get("updated") - .unwrap_or(entry_published_raw) - })) - .unwrap(); - - let published = timeparse::parse(entry_published_raw).unwrap(); - - AtomEntry { - relpath, - id, - title, - published, - updated, - } -} diff --git a/src/atom/urn.rs b/src/atom/urn.rs deleted file mode 100644 index 9ec73ae..0000000 --- a/src/atom/urn.rs +++ /dev/null @@ -1,49 +0,0 @@ -use core::fmt; - -use rand::{rngs::OsRng, Rng}; - -//TODO: gen- Check URN are valid -// https://www.rfc-editor.org/rfc/rfc2141#section-2.1 -pub struct Urn { - /// Namespace Identifier - nid: String, - /// Namespace Specific string. This is the unique part of the identifier - nss: String, -} - -impl Urn { - /// awake specific function for generating a URN in the form of: - /// `urn:NID:1234-5678` - /// Where 1234-5678 is a randomly generated eight digit ID broken in two. - pub fn new_random>(nid: S) -> Self { - let first = random_base58(4); - let second = random_base58(4); - - Self { - nid: nid.into(), - nss: format!("{first}-{second}"), - } - } -} - -impl fmt::Display for Urn { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Urn { nid, nss } = self; - - write!(f, "urn:{nid}:{nss}") - } -} - -const BASE58: &'static [u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; -const USER_ID_LENGTH: usize = 6; -const SESSION_ID_LENGTH: usize = 12; - -/// Random Base58 string, `count` characters long, using OsRng which is assumed -/// to be secure -/// > assumed that system always provides high-quality cryptographically secure random data -fn random_base58(count: usize) -> String { - let mut rng = OsRng::default(); - std::iter::from_fn(|| Some(BASE58[rng.gen_range(0..BASE58.len())] as char)) - .take(count) - .collect() -} diff --git a/src/atomizer.rs b/src/atomizer.rs deleted file mode 100755 index e6a147b..0000000 --- a/src/atomizer.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::str::FromStr; - -use bempline::{variables, Document, Options}; -use camino::{Utf8Path, Utf8PathBuf}; -use confindent::{Confindent, Value}; -use cutie::Html; -use time::format_description::well_known; - -use crate::{markup, templated::Templated, timeparse}; - -pub fn main() -> ! { - let awake_conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap(); - let atom_conf_path = std::env::args().nth(3).unwrap_or(String::from("atom.conf")); - - let atom_conf = Confindent::from_file(atom_conf_path).unwrap(); - - let root: Utf8PathBuf = awake_conf.child_parse("Webroot").unwrap(); - let atom = Document::from_file( - root.clone() - .join(atom_conf.child_value("Template").unwrap()) - .canonicalize_utf8() - .unwrap(), - Options::default(), - ) - .unwrap(); - - let feeds = atom_conf.children("Feed"); - for feed in feeds { - make_feed(feed, &root, atom.clone()) - } - - std::process::exit(0); -} - -pub fn make_feed(feed: &Value, root: &Utf8Path, mut atom: Document) { - let feed_relout: Utf8PathBuf = feed.child_parse("Output").unwrap(); - let feed_link = feed.child_owned("Link").unwrap(); - let feed_id = feed.child_owned("Id").unwrap(); - let feed_title = feed.child_owned("Title").unwrap(); - let feed_subtitle = feed.child_owned("Subtitle").unwrap(); - let feed_updated = feed.child_owned("Updated").unwrap(); - let feed_author_name = feed.get("Author/Name").unwrap(); - let feed_author_email = feed.get("Author/Email").unwrap(); - - variables!( - atom, - feed_link, - feed_id, - feed_title, - feed_subtitle, - feed_updated, - feed_author_email, - feed_author_name - ); - - let entries = feed.children("Entry"); - let entry_pattern = atom.get_pattern("entry").unwrap(); - for entry in entries { - let entry_relpath: Utf8PathBuf = entry.parse().unwrap(); - let entry_path = root.join(&entry_relpath); - - println!("entry {entry_relpath}"); - - let entry_content = match std::fs::read_to_string(&entry_path) { - Ok(ok) => ok, - Err(e) => { - panic!("failed to get file at {entry_path}: {e}"); - } - }; - let entry_templated = Templated::from_str(&entry_content).unwrap(); - - let entry_title = entry - .child_value("Title") - .unwrap_or_else(|| entry_templated.frontmatter.get("title").unwrap()); - let entry_link = entry.child_value("Link").unwrap(); - let entry_id = entry.child_value("Id").unwrap(); - - let entry_published_raw = entry - .child_value("Published") - .unwrap_or_else(|| entry_templated.frontmatter.get("published").unwrap()); - - let entry_updated = timeparse::parse(entry.child_value("Updated").unwrap_or_else(|| { - entry_templated - .frontmatter - .get("updated") - .unwrap_or(entry_published_raw) - })) - .unwrap() - .format(&well_known::Rfc3339) - .unwrap(); - - let entry_published = timeparse::parse(entry_published_raw) - .unwrap() - .format(&well_known::Rfc3339) - .unwrap(); - - let mut entry_content = markup::process(&entry_templated.content); - - if let Some(part_id) = entry - .child_value("Part") - .map(|raw| raw.strip_prefix('#')) - .flatten() - { - let cutie = cutie::Html::parse(entry_content); - let found = cutie.get_by_id(part_id).unwrap(); - entry_content = found.to_string(); - } - - let mut pattern = entry_pattern.clone(); - variables!( - pattern, - entry_title, - entry_link, - entry_id, - entry_published, - entry_updated - ); - pattern.set("entry_content", cdataify(&entry_content)); - atom.set_pattern("entry", pattern); - } - - let string = atom.compile(); - - let feed_out = root.join(feed_relout); - std::fs::write(feed_out, string).unwrap(); -} - -//FIXME: do NOT use this name -fn htmlspecialchars(raw: &str) -> String { - let mut ret = String::new(); - - for ch in raw.chars() { - match ch { - '<' => ret.push_str("<"), - '>' => ret.push_str(">"), - '&' => ret.push_str("&"), - '\'' => ret.push_str("'"), - '"' => ret.push_str("""), - c => ret.push(c), - } - } - - ret -} - -fn cdataify(raw: &str) -> String { - format!("") -} - -fn fix_links(html: &mut Html, path: Utf8PathBuf) { - let root = match path.file_name() { - Some(_) => path.parent().unwrap().to_owned(), - None => path, - }; - - for tag in html.child_tags_mut() { - if let Some(src) = tag.get_attribute("src") { - if !src.starts_with("http") { - todo!() - } - } - } -} diff --git a/src/db.rs b/src/db.rs deleted file mode 100644 index 0abf722..0000000 --- a/src/db.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::sync::Mutex; - -use camino::Utf8PathBuf; -use rusqlite::{params, Connection, OptionalExtension}; -use time::OffsetDateTime; - -use crate::gatherer::{Cpuinfo, Meminfo}; - -pub struct Database { - db_path: Utf8PathBuf, - conn: Mutex, -} - -impl Database { - pub fn new(db_path: Utf8PathBuf) -> Self { - Self { - conn: Mutex::new(Connection::open(&db_path).unwrap()), - db_path, - } - } - - pub fn create_tables(&self) { - let conn = self.conn.lock().unwrap(); - conn.execute(CREATE_TABLE_HOSTMEM, params![]).unwrap(); - conn.execute(CREATE_TABLE_HOSTNET, params![]).unwrap(); - conn.execute(CREATE_TABLE_HOSTCPU, params![]).unwrap(); - } - - pub fn insert_host_meminfo(&self, meminfo: Meminfo) { - let conn = self.conn.lock().unwrap(); - - conn.execute( - "INSERT INTO stats_hostmem(total_kb, available_kb) VALUES (?1, ?2)", - params![meminfo.total, meminfo.avaialable], - ) - .unwrap(); - } - - pub fn get_last_host_meminfo(&self) -> DbMeminfo { - let conn = self.conn.lock().unwrap(); - - conn.query_row( - "SELECT * FROM stats_hostmem ORDER BY stamp DESC LIMIT 1", - [], - |row| { - let (stamp, total_kb, available_kb) = row.try_into().unwrap(); - - Ok(DbMeminfo { - stamp, - total_kb, - available_kb, - }) - }, - ) - .optional() - .unwrap() - .unwrap() - } - - pub fn get_last_n_host_meminfo(&self, count: usize) -> Vec { - let conn = self.conn.lock().unwrap(); - let mut stmt = conn - .prepare("SELECT * FROM stats_hostmem ORDER BY stamp DESC LIMIT ?1") - .unwrap(); - - stmt.query_map(params![count], |row| { - Ok(DbMeminfo { - stamp: row.get(0)?, - total_kb: row.get(1)?, - available_kb: row.get(2)?, - }) - }) - .unwrap() - .map(|r| r.unwrap()) - .collect() - } - - pub fn get_date_bound_n_host_meminfo( - &self, - since: OffsetDateTime, - until: OffsetDateTime, - count: usize, - ) -> Vec { - let conn = self.conn.lock().unwrap(); - let mut stmt = conn - .prepare("SELECT * FROM stats_hostmem WHERE stamp > ?1 AND stamp < ?2 ORDER BY stamp DESC LIMIT ?3") - .unwrap(); - - stmt.query_map(params![since, until, count], |row| { - Ok(DbMeminfo { - stamp: row.get(0)?, - total_kb: row.get(1)?, - available_kb: row.get(2)?, - }) - }) - .unwrap() - .map(|r| r.unwrap()) - .collect() - } - - pub fn insert_hostnet(&self, span_sec: usize, rx_delta: usize, tx_delta: usize) { - let conn = self.conn.lock().unwrap(); - - conn.execute( - "INSERT INTO stats_hostnet(span_sec, rx_delta, tx_delta) VALUES (?1, ?2, ?3)", - params![span_sec, rx_delta, tx_delta], - ) - .unwrap(); - } - - pub fn get_last_n_hostnet(&self, count: usize) -> Vec { - let conn = self.conn.lock().unwrap(); - let mut stmt = conn - .prepare("SELECT * FROM stats_hostnet ORDER BY stamp DESC LIMIT ?1") - .unwrap(); - - stmt.query_map(params![count], |row| { - Ok(DbNetinfo { - stamp: row.get(0)?, - span_sec: row.get(1)?, - rx_delta: row.get(2)?, - tx_delta: row.get(3)?, - }) - }) - .unwrap() - .map(|r| r.unwrap()) - .collect() - } - - pub fn insert_hostcpu( - &self, - span_sec: usize, - user_delta: usize, - nice_delta: usize, - system_delta: usize, - ) { - let conn = self.conn.lock().unwrap(); - - conn.execute( - "INSERT INTO stats_hostcpu(span_sec, user_delta, nice_delta, system_delta) VALUES (?1, ?2, ?3, ?4)", - params![span_sec, user_delta, nice_delta, system_delta], - ) - .unwrap(); - } - - pub fn get_last_n_hostcpu(&self, count: usize) -> Vec { - let conn = self.conn.lock().unwrap(); - let mut stmt = conn - .prepare("SELECT * FROM stats_hostcpu ORDER BY stamp DESC LIMIT ?1") - .unwrap(); - - stmt.query_map(params![count], |row| { - Ok(DbCpuinfo { - stamp: row.get(0)?, - span_sec: row.get(1)?, - user_delta: row.get(2)?, - nice_delta: row.get(3)?, - system_delta: row.get(4)?, - }) - }) - .unwrap() - .map(|r| r.unwrap()) - .collect() - } -} - -pub const CREATE_TABLE_HOSTMEM: &'static str = "\ - CREATE TABLE IF NOT EXISTS stats_hostmem ( - stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - total_kb INTEGER NOT NULL, - available_kb INTEGER NOT NULL - );"; - -pub const CREATE_TABLE_HOSTNET: &'static str = "\ - CREATE TABLE IF NOT EXISTS stats_hostnet( - stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - span_sec INTEGER NOT NULL, - rx_delta INTEGER NOT NULL, - tx_delta INTEGER NOT NULL - );"; - -pub const CREATE_TABLE_HOSTCPU: &'static str = "\ - CREATE TABLE IF NOT EXISTS stats_hostcpu( - stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - span_sec INTEGER NOT NULL, - user_delta INTEGER NOT NULL, - nice_delta INTEGER NOT NULL, - system_delta INTEGER NOT NULL - );"; - -#[derive(Clone, Copy, Debug)] -pub struct DbMeminfo { - pub stamp: OffsetDateTime, - pub total_kb: usize, - pub available_kb: usize, -} - -impl DbMeminfo { - pub fn usage(&self) -> usize { - self.total_kb - self.available_kb - } -} - -#[derive(Copy, Clone, Debug)] -pub struct DbNetinfo { - pub stamp: OffsetDateTime, - pub span_sec: usize, - pub rx_delta: usize, - pub tx_delta: usize, -} - -impl DbNetinfo { - pub fn rx_bytes_per_sec(&self) -> f32 { - self.rx_delta as f32 / self.span_sec as f32 - } - - pub fn tx_bytes_per_sec(&self) -> f32 { - self.tx_delta as f32 / self.span_sec as f32 - } -} - -#[derive(Copy, Clone, Debug)] -pub struct DbCpuinfo { - pub stamp: OffsetDateTime, - pub span_sec: usize, - pub user_delta: usize, - pub nice_delta: usize, - pub system_delta: usize, -} - -impl DbCpuinfo { - /// Returns the avarage usage still in USER_HZ which is - /// in 1/100ths sec on "most systems". This is averaged - /// across the minute sample rate, so unit is: - /// USER_HZ/s - pub fn average_usage(&self) -> f32 { - let used_sum = self.user_delta + self.nice_delta + self.system_delta; - let avg = used_sum as f32 / self.span_sec as f32; - avg - } - - /// The sum of the user, nice, and system deltas - pub fn usage_sum(&self) -> usize { - self.user_delta + self.nice_delta + self.system_delta - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100755 index 56539d0..0000000 --- a/src/error.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::io; - -use camino::Utf8PathBuf; - -#[derive(Debug, snafu::Snafu)] -pub enum RuntimeError { - #[snafu(display("the path was not found: {path}"))] - NotFound { - source: io::Error, - path: Utf8PathBuf, - }, - #[snafu(display("io error: {path}: {source}"))] - UnknownIo { - source: io::Error, - path: Utf8PathBuf, - }, - #[snafu(display("path tried to go below webroot: {path}"))] - PathTooLow { path: String }, -} - -impl RuntimeError { - pub fn from_io(source: io::Error, path: Utf8PathBuf) -> Self { - match source.kind() { - io::ErrorKind::NotFound => RuntimeError::NotFound { source, path }, - _ => RuntimeError::UnknownIo { source, path }, - } - } -} diff --git a/src/fs.rs b/src/fs.rs deleted file mode 100755 index e8044a0..0000000 --- a/src/fs.rs +++ /dev/null @@ -1,325 +0,0 @@ -use camino::{Utf8Path, Utf8PathBuf}; -use std::{fmt, io, ops::Deref, path::Display, str::FromStr}; - -use crate::RuntimeError; - -/// Webpath is the path we get from HTTP requests. It's garunteed to not fall -/// below the webroot and will never start or end with a slash. -#[derive(Clone, Debug, PartialEq)] -pub struct Webpath { - pub webcanon: Utf8PathBuf, - is_dir: bool, -} - -impl FromStr for Webpath { - type Err = RuntimeError; - - fn from_str(raw: &str) -> Result { - let is_dir = raw.trim_end().ends_with('/'); - let mut curr = Utf8PathBuf::new(); - - for component in raw.split('/') { - match component { - "." => continue, - ".." => { - if !curr.pop() { - return Err(RuntimeError::PathTooLow { - path: raw.to_owned(), - }); - } - } - comp => curr.push(comp), - } - } - - Ok(Self { - webcanon: curr, - is_dir, - }) - } -} - -impl Webpath { - /// Return whether or not this Webpath is empty which would indicate it's - /// the homepage. - pub fn is_index(&self) -> bool { - self.webcanon == "" - } - - /// Whether or not this Webpath points to a directory (has a trailing slash) - pub fn is_dir(&self) -> bool { - self.is_dir - } - - /// Return a String that interprets this Webpath as a directory, adding a - /// trailing slash - pub fn as_dir(&self) -> String { - format!("/{}/", self.webcanon) - } - - /// Returns the first directory of the Webpath. Assumes the path is a - /// directory if it ends in a `/`, otherwise pops the last component. - pub fn first_dir(&self) -> String { - if self.is_dir { - self.webcanon.to_string() - } else { - let mut dir = self.webcanon.clone(); - dir.pop(); - dir.to_string() - } - } -} - -impl Deref for Webpath { - type Target = Utf8Path; - - fn deref(&self) -> &Self::Target { - &self.webcanon - } -} - -impl AsRef for Webpath { - fn as_ref(&self) -> &Utf8Path { - &self.webcanon - } -} - -impl PartialEq for Webpath { - fn eq(&self, other: &str) -> bool { - self.webcanon.eq(other) - } -} - -impl fmt::Display for Webpath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "/{}", self.webcanon) - } -} - -const ROOT_INDEX: &str = "home.html"; -const DIRFILE_EXT: &str = "html"; - -pub struct PathResolution { - pub filepath: Utf8PathBuf, - pub is_dirfile: bool, -} - -impl Deref for PathResolution { - type Target = Utf8Path; - - fn deref(&self) -> &Self::Target { - self.filepath.as_path() - } -} - -impl fmt::Display for PathResolution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.deref()) - } -} - -#[derive(Clone, Debug)] -pub struct Filesystem { - webroot: Utf8PathBuf, -} - -impl Filesystem { - pub fn new>(root: P) -> Self { - Self { - webroot: root.into(), - } - } - - pub fn resolve(&self, webpath: &Webpath) -> Result { - if webpath.is_index() || webpath == ROOT_INDEX { - return Ok(PathResolution { - filepath: self.webroot.join(ROOT_INDEX), - is_dirfile: false, - }); - } - - let path = self.webroot.join(webpath); - let metadata = Self::metadata(&path)?; - - // If it's a file then return the path immediatly - if metadata.is_file() { - Ok(PathResolution { - filepath: path, - is_dirfile: false, - }) - } else { - // we check this above. but still.. - //TODO: gen- probably don't unwrap - let filename = path.file_name().unwrap(); - let dirfile = path.join(format!("{filename}.{DIRFILE_EXT}")); - - if dirfile.exists() { - Ok(PathResolution { - filepath: dirfile, - is_dirfile: true, - }) - } else { - Err(RuntimeError::NotFound { - source: io::ErrorKind::NotFound.into(), - path: dirfile, - }) - } - } - } - - /// Resolve a file system path to a webpath. - /// - /// Paths are not checked to see if they exist and are not checked to be within the webroot. - /// - /// Paths attempt to resolve to the simplest form. For example, if the path is [`ROOT_INDEX`] - /// it will come out as `/` and if it is a dirfile, it will come out as a directory. - /// - /// **FOOTGUN** currently assumes every path is a file... - pub fn reverse_resolve>(&self, path: P) -> Result { - let path = path.as_ref(); - let relpath = match path.strip_prefix(&self.webroot) { - Ok(relpath) => relpath, - Err(_e) => { - if path.is_relative() { - path - } else { - //FIXME: gen- This error is not strictly correct, but it'll do for now. - // 2024-10-12 - return Err(RuntimeError::PathTooLow { - path: path.to_string(), - }); - } - } - }; - - if relpath == "" || relpath == ROOT_INDEX { - return Ok(Webpath { - webcanon: Utf8PathBuf::new(), - is_dir: true, - }); - } - - todo!(); - - Ok(Webpath { - webcanon: relpath.into(), - is_dir: false, - }) - } - - pub fn metadata>(path: P) -> Result { - path.as_ref() - .metadata() - .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) - } - - pub async fn open>(path: P) -> Result { - tokio::fs::File::open(path.as_ref()) - .await - .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) - } - - pub async fn read>(path: P) -> Result, RuntimeError> { - tokio::fs::read(path.as_ref()) - .await - .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) - } - - pub async fn read_to_string>(path: P) -> Result { - tracing::trace!("read_to_string {}", path.as_ref()); - - tokio::fs::read_to_string(path.as_ref()) - .await - .map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned())) - } -} - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use crate::fs::{Webpath, ROOT_INDEX}; - - use super::Filesystem; - - macro_rules! webpath { - ($location:expr) => { - Webpath::from_str($location).unwrap() - }; - } - - #[test] - fn webpath_finds_too_low() { - assert!(Webpath::from_str("/..").is_err()); - assert!(Webpath::from_str("/one/..").is_ok()); - assert!(Webpath::from_str("/one/../..").is_err()); - assert!(Webpath::from_str("/one/../two").is_ok()) - } - - #[test] - fn webpath_path_correct() { - assert_eq!(Webpath::from_str("/one").unwrap().webcanon, "one"); - assert_eq!(Webpath::from_str("/one/..").unwrap().webcanon, ""); - assert_eq!(Webpath::from_str("/one/two/..").unwrap().webcanon, "one"); - assert_eq!(Webpath::from_str("/one/../two").unwrap().webcanon, "two"); - assert_eq!( - Webpath::from_str("/one/two/three/..").unwrap().webcanon, - "one/two" - ); - assert_eq!( - Webpath::from_str("/one/../home.html").unwrap().webcanon, - "home.html" - ); - } - - const TESTROOT: &str = "test/serve"; - - #[test] - fn filesystem_resolves_index() { - let fs = Filesystem::new(TESTROOT); - assert_eq!( - fs.resolve(&webpath!("/")).unwrap().filepath, - format!("{TESTROOT}/{ROOT_INDEX}") - ); - assert_eq!( - fs.resolve(&webpath!("/one/..")).unwrap().filepath, - format!("{TESTROOT}/{ROOT_INDEX}") - ); - } - - #[test] - fn filesystem_resolves_dirfile() { - let fs = Filesystem::new(TESTROOT); - assert_eq!( - fs.resolve(&webpath!("/one")).unwrap().filepath, - format!("{TESTROOT}/one/one.html") - ); - assert_eq!( - fs.resolve(&webpath!("/one/eleven/")).unwrap().filepath, - format!("{TESTROOT}/one/eleven/eleven.html") - ); - assert_eq!( - fs.resolve(&webpath!("/one/eleven/..")).unwrap().filepath, - format!("{TESTROOT}/one/one.html") - ); - } - - #[test] - fn filesystem_reverse_resolves_index() { - let fs = Filesystem::new(TESTROOT); - - assert_eq!(fs.reverse_resolve(TESTROOT).unwrap(), webpath!("/")); - assert_eq!( - fs.reverse_resolve(format!("{TESTROOT}/{ROOT_INDEX}")) - .unwrap(), - webpath!("/") - ) - } - - #[test] - fn filesystem_reverse_resolves_directories() { - let fs = Filesystem::new(TESTROOT); - - assert_eq!(fs.reverse_resolve("test/").unwrap(), webpath!("test/")) - } -} diff --git a/src/gatherer.rs b/src/gatherer.rs deleted file mode 100644 index e6bae6f..0000000 --- a/src/gatherer.rs +++ /dev/null @@ -1,429 +0,0 @@ -use std::{ - array, - fs::File, - io::{BufRead, BufReader}, - sync::{atomic::Ordering, mpsc::Sender}, - thread::JoinHandle, - time::Duration, -}; - -use regex_lite::Regex; -use time::OffsetDateTime; - -use crate::{ - db::DbMeminfo, - griph::{self, Style, TwoLineOrder}, - AwakeState, -}; - -pub struct Gatherer { - state: AwakeState, - hwnd: Option>, -} - -impl Gatherer { - pub fn new(state: AwakeState) -> Self { - Self { state, hwnd: None } - } - - pub fn start(&mut self) { - let state = self.state.clone(); - let hwnd = std::thread::spawn(|| task(state)); - self.hwnd = Some(hwnd); - } -} - -fn task(state: AwakeState) { - tracing::info!("starting gatherer thread"); - - if !state.do_statistics { - tracing::warn!("statistics disabled"); - return; - } - - // I just want a graph on first boot; don't care about divisions just yet - make_mem_graph(&state); - make_net_graph(&state); - make_cpu_graph(&state); - - // If we collected a point less than a minute ago, like after - // just being restarted, wait until it's been a minute - let last_meminfo = state.database.get_last_host_meminfo(); - let since = OffsetDateTime::now_utc() - last_meminfo.stamp; - if since < time::Duration::minutes(1) { - let to_minute = time::Duration::minutes(1) - since; - - tracing::info!( - "waiting for {}s to space a minute apart", - to_minute.whole_seconds() - ); - std::thread::sleep(to_minute.try_into().unwrap()); - } - - let mut last_netinfo: Option = None; - let mut last_cpuinfo: Option = None; - - // this is a `let _` because otherwise the attribute was - // making the comiler mad - #[rustfmt::skip] - let _ = loop { - tracing::debug!("collecting stats"); - - // Gather data - let meminfo = Meminfo::current(); - let netinfo = Netinfo::current(); - let cpuinfo = Cpuinfo::current(); - - // Print traces, y'know, for tracing - tracing::trace!("memory: {}MB used / {}MB total", meminfo.usage() / 1000, meminfo.total / 1000); - tracing::trace!("net: rx {} / tx {}", data_human_fmt(netinfo.rx_bytes), data_human_fmt(netinfo.tx_bytes)); - tracing::trace!("cpu: user {} // nice {} // system {}", cpuinfo.user, cpuinfo.nice, cpuinfo.system); - - // Store stats in database - if let Some(lni) = last_netinfo { - let rx_delta = netinfo.rx_bytes - lni.rx_bytes; - let tx_delta = netinfo.tx_bytes - lni.tx_bytes; - - state.database.insert_hostnet(60, rx_delta, tx_delta); - } - last_netinfo = Some(netinfo); - - if let Some(lci) = last_cpuinfo { - let user_delta = cpuinfo.user - lci.user; - let nice_delta = cpuinfo.nice - lci.nice; - let system_delta = cpuinfo.system - lci.system; - - state.database.insert_hostcpu(60, user_delta, nice_delta, system_delta); - } - last_cpuinfo = Some(cpuinfo); - - state.database.insert_host_meminfo(meminfo); - - // Only generate graphs every 15 minutes - let now = OffsetDateTime::now_utc(); - if now.minute() % 15 == 0 { - make_mem_graph(&state); - make_net_graph(&state); - make_cpu_graph(&state); - } - - std::thread::sleep(Duration::from_secs(60)); - }; -} - -pub fn make_mem_graph(state: &AwakeState) { - tracing::debug!("generating meminfo graph"); - - let infos = state.database.get_last_n_host_meminfo(256); - let max = infos[0].total_kb; - - let now = get_clean_now(); - let cleaned = clean_series(&infos, |mem| mem.stamp, now); - let mut usages: Vec> = cleaned - .into_iter() - .map(|mi| mi.map(|mi| mi.usage())) - .collect(); - - // Reversing here because we want latest valeus on on the - // right side, so last in the array - usages.reverse(); - - let gif = griph::make_1line(0, max, &usages, Style::Discontinuous); - - let path = state.cache_path.join("current_hostmeminfo.gif"); - gif.save(path).unwrap(); -} - -const NET_INCREMENTS: usize = 256; - -pub fn make_net_graph(state: &AwakeState) { - tracing::debug!("generating netinfo graph"); - - let now = get_clean_now(); - let infos = state.database.get_last_n_hostnet(256); - let cleaned = clean_series(&infos, |net| net.stamp, now); - - // 125 is (1000 / 8) so it converst Bytes to kiloBITS - let mut rx_deltas = extract(&cleaned, |ni| ni.rx_bytes_per_sec() as usize / 125); - let mut tx_deltas = extract(&cleaned, |ni| ni.tx_bytes_per_sec() as usize / 125); - - // Reversing to put latest values on the right side - rx_deltas.reverse(); - tx_deltas.reverse(); - - let rx_zeroed: Vec = cleaned - .iter() - .map(|m| m.map(|n| n.rx_bytes_per_sec() as usize / 125).unwrap_or(0)) - .collect(); - - let tx_zeroed: Vec = cleaned - .iter() - .map(|m| m.map(|n| n.tx_bytes_per_sec() as usize / 125).unwrap_or(0)) - .collect(); - - let rx_sum = rx_zeroed.iter().fold(0, |acc, x| acc + x); - let tx_sum = tx_zeroed.iter().fold(0, |acc, x| acc + x); - - // Mixing the TX/RX delta so we can pick a range. - let mut mixed = vec![0; 512]; - mixed[..256].copy_from_slice(&rx_zeroed); - mixed[256..].copy_from_slice(&tx_zeroed); - - mixed.sort(); - let kinda_highest = mixed[511 - 32]; - // We add one here so we have less "surface area" covered by the lines - // which makes the vertical divisions look btter - let high_bound_whole = (kinda_highest as f32 / NET_INCREMENTS as f32) - .ceil() - .max(1.0) as usize; - - let high_bound = if high_bound_whole > 1 { - high_bound_whole + 1 - } else { - high_bound_whole - } * NET_INCREMENTS; - - state - .netinfo_upper_bound - .store(high_bound, Ordering::Release); - - let order = if tx_sum > rx_sum { - TwoLineOrder::SeriesAFirst - } else { - TwoLineOrder::SeriesBFirst - }; - - tracing::debug!("tx_sum = {tx_sum} // rx_sum = {rx_sum} // order = {order:?}"); - - /*for (idx, (tx, rx)) in tx_deltas.iter().zip(rx_deltas).enumerate() { - println!( - "[{idx:<03}] tx = {} / rx = {}", - tx.unwrap_or_default(), - rx.unwrap_or_default() - ); - }*/ - - let gif = griph::make_2line(0, high_bound, &tx_deltas, &rx_deltas, order); - - let path = state.cache_path.join("current_hostnetinfo.gif"); - gif.save(path).unwrap(); -} - -pub fn make_cpu_graph(state: &AwakeState) { - tracing::debug!("generating cpuinfo graph"); - - let infos = state.database.get_last_n_hostcpu(256); - - let now = get_clean_now(); - let cleaned = clean_series(&infos, |cpu| cpu.stamp, now); - - // Usages are of the unit of hundreths of a second of CPU usage averaged over a minute. - // A value of 1 means the cpu saw 1% CPU usage - let usages = extract(&cleaned, |cpu| cpu.average_usage()); - - let mut zeroed: Vec = usages.iter().map(|m| m.unwrap_or(0.0)).collect(); - zeroed.sort_by(|a, b| a.partial_cmp(&b).unwrap()); - let kinda_highest = zeroed[255 - 8]; - - tracing::debug!( - "kinda_highest = {kinda_highest} // highest = {}", - zeroed[255] - ); - - // high_vound unit: hundreths of a second (1% / 1) - // scaling by 1.33 here to drive the line down to decrease the apparent - // "surface area" of the line - let high_bound = (kinda_highest * 1.33).max(1.0); - state - .cpuinfo_upper_bound - .store(high_bound as usize, Ordering::Release); - - // can't scale the range, so we have to scale the inputs. - // Finally, we multiply by ten to put it in a 0-1000 range - let scale_factor = 100.0 / high_bound; - let mut scaled: Vec> = usages - .iter() - .map(|m| m.map(|v| (v * scale_factor) as usize * 10)) - .collect(); - - // Reversing here because we want latest valeus on on the - // right side, so last in the array - scaled.reverse(); - - let gif = griph::make_1line(0, 1000, &scaled, Style::Line); - - let path = state.cache_path.join("current_hostcpuinfo.gif"); - gif.save(path).unwrap(); -} - -fn get_clean_now() -> OffsetDateTime { - OffsetDateTime::now_utc().replace_second(0).unwrap() -} - -fn clean_series(series: &[T], time_extractor: F, end_time: OffsetDateTime) -> [Option; 256] -where - F: Fn(&T) -> OffsetDateTime, - T: Clone, -{ - let mut res = [const { None }; 256]; - - for value in series { - let time = time_extractor(value); - let delta = end_time - time; - let mins = delta.whole_minutes(); - - if mins >= 0 && mins < 256 { - res[mins as usize] = Some(value.clone()); - } - } - - res -} - -fn extract(series: &[Option], extractor: F) -> [Option; 256] -where - F: Fn(&T) -> V, -{ - let mut res = [const { None }; 256]; - for (idx, maybe) in series.iter().enumerate() { - if let Some(value) = maybe { - res[idx] = Some(extractor(value)); - } - } - - res -} - -pub struct Meminfo { - pub total: usize, - pub free: usize, - pub avaialable: usize, -} - -impl Meminfo { - pub fn current() -> Self { - let procinfo = File::open("/proc/meminfo").unwrap(); - let bread = BufReader::new(procinfo); - - let mut meminfo = Meminfo { - total: 0, - free: 0, - avaialable: 0, - }; - - for line in bread.lines() { - let line = line.unwrap(); - - if let Some((raw_key, raw_value_kb)) = line.split_once(':') { - let value = if let Some(raw_value) = raw_value_kb.trim().strip_suffix(" kB") { - if let Ok(parsed) = raw_value.parse() { - parsed - } else { - continue; - } - } else { - continue; - }; - - match raw_key.trim() { - "MemTotal" => meminfo.total = value, - "MemFree" => meminfo.free = value, - "MemAvailable" => meminfo.avaialable = value, - _ => (), - } - } - } - - meminfo - } - - pub fn usage(&self) -> usize { - self.total - self.avaialable - } -} - -pub struct Netinfo { - rx_bytes: usize, - tx_bytes: usize, -} - -impl Netinfo { - pub fn current() -> Self { - let procinfo = File::open("/proc/net/dev").unwrap(); - let bread = BufReader::new(procinfo); - - let mut netinfo = Self { - rx_bytes: 0, - tx_bytes: 0, - }; - - let interface = "eth0:"; - for line in bread.lines() { - let line = line.unwrap(); - let trim = line.trim(); - - if let Some(data) = trim.strip_prefix(interface) { - let mut splits = data.split_whitespace(); - netinfo.rx_bytes = splits.next().unwrap().parse().unwrap(); - netinfo.tx_bytes = splits.skip(7).next().unwrap().parse().unwrap(); - break; - } - } - - netinfo - } -} - -/// Not strictly the correct name for this, but that's fine. -/// Structure for parsed-from /proc/stat -pub struct Cpuinfo { - user: usize, - nice: usize, - system: usize, -} - -impl Cpuinfo { - pub fn current() -> Self { - let procinfo = File::open("/proc/stat").unwrap(); - let bread = BufReader::new(procinfo); - - let mut cpuinfo = Cpuinfo { - user: 0, - nice: 0, - system: 0, - }; - - for line in bread.lines() { - let line = line.unwrap(); - let Some(data) = line.strip_prefix("cpu ") else { - continue; - }; - - let mut splits = data.split(' '); - cpuinfo.user = splits.next().unwrap().parse().unwrap(); - cpuinfo.nice = splits.next().unwrap().parse().unwrap(); - cpuinfo.system = splits.next().unwrap().parse().unwrap(); - } - - cpuinfo - } -} - -fn data_human_fmt(bytes: usize) -> String { - let (num, unit) = data_human(bytes); - format!("{num}{unit}") -} - -fn data_human(bytes: usize) -> (f32, &'static str) { - const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB"]; - - let mut wrk = bytes as f32; - let mut unit_idx = 0; - loop { - if wrk < 1500.0 || unit_idx == UNITS.len() - 1 { - return (wrk, UNITS[unit_idx]); - } - wrk /= 1000.0; - unit_idx += 1; - } -} diff --git a/src/griph/mod.rs b/src/griph/mod.rs deleted file mode 100644 index ef2d1ee..0000000 --- a/src/griph/mod.rs +++ /dev/null @@ -1,236 +0,0 @@ -use gifed::{writer::ImageBuilder, Gif, StandardGif}; - -#[rustfmt::skip] -pub const DARK_PALETTE: &[u8] = &[ - 0, 0, 0, // Background - Black - 192, 192, 192, // Graphline - Mostly White - 64, 64, 64, // Gridlines - Dark gray - 32, 32, 32, // Minor Gridlines - Darker gray - 144, 144, 255, // Primary 2 Colour - Blue - 48, 192, 48, // Secondary 2 Colour - Green - 96, 96, 224, // Primary Underfill - Light Blue - 48, 128, 48, // Secondary Underfill - Lesser Green - 255, 255, 255, // Graphline Underfill - 16 * 9 gray - 192, 128, 128 // Missing Data Red - lightish red -]; - -const BACKGROUND: u8 = 0; -const LINE: u8 = 1; -const GRIDLINE: u8 = 2; -const MINOR_GRIDLINE: u8 = 3; -const LINE1: u8 = 4; -const LINE2: u8 = 5; -const LINE1_FILL: u8 = 6; -const LINE2_FILL: u8 = 7; -const LINE_FILL: u8 = 8; -const MISSING: u8 = 9; - -const WIDTH: usize = 256; -const HEIGHT: usize = 160; -const SIZE: usize = WIDTH * HEIGHT; - -pub enum Style { - Discontinuous, - Line, - UnderfilledLine, -} - -pub fn make_1line(min: usize, max: usize, values: &[Option], style: Style) -> Gif { - let range = max - min; - // this assumes a range of values that is >1 per pixel - let vpp = range as f32 / HEIGHT as f32; - - let mut raster = vec![0; SIZE]; - let mut standard = Gif::new(WIDTH as u16, HEIGHT as u16); - - draw_grid(&mut raster); - draw_line(&mut raster, values, vpp, LINE); - - let plt = match style { - Style::Discontinuous => { - draw_line(&mut raster, values, vpp, LINE); - DARK_PALETTE //[0..12] - } - Style::Line => { - draw_line_continuous(&mut raster, values, vpp, LINE); - DARK_PALETTE //[0..12] - } - Style::UnderfilledLine => { - draw_line_underfill(&mut raster, values, vpp, LINE_FILL, LINE); - DARK_PALETTE - } - }; - - standard.set_palette(Some(plt.try_into().unwrap())); - standard.push( - ImageBuilder::new(WIDTH as u16, HEIGHT as u16) - .build(raster) - .unwrap(), - ); - - standard -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum TwoLineOrder { - SeriesAFirst, - SeriesBFirst, -} - -pub fn make_2line( - min: usize, - max: usize, - values1: &[Option], - values2: &[Option], - draw_order: TwoLineOrder, -) -> Gif { - let range = max - min; - // this assumes a range of values that is >1 per pixel - let vpp = range as f32 / HEIGHT as f32; - - let mut raster = vec![0; SIZE]; - draw_grid(&mut raster); - - match draw_order { - TwoLineOrder::SeriesAFirst => { - draw_line_underfill(&mut raster, values1, vpp, LINE1, LINE1_FILL); - draw_line_underfill(&mut raster, values2, vpp, LINE2, LINE2_FILL); - } - TwoLineOrder::SeriesBFirst => { - draw_line_underfill(&mut raster, values2, vpp, LINE2, LINE2_FILL); - draw_line_underfill(&mut raster, values1, vpp, LINE1, LINE1_FILL); - } - } - - let mut standard = Gif::new(WIDTH as u16, HEIGHT as u16); - standard.set_palette(Some(DARK_PALETTE.try_into().unwrap())); - standard.push( - ImageBuilder::new(WIDTH as u16, HEIGHT as u16) - .build(raster) - .unwrap(), - ); - - standard -} - -fn draw_grid(raster: &mut [u8]) { - // Draw Divisions - // we want a gridline every 16 pixels, but not the bottom - // or top, so only 8. - for div in 1..=9 { - let y_val = div * 16; - - let grid = if div % 2 == 0 { - GRIDLINE - } else { - MINOR_GRIDLINE - }; - - for x in 0..WIDTH { - raster[y_val * WIDTH + x] = grid; - } - } - - // Draw vertical divisions every 30 pixels (minutes) - for div in 1..=8 { - let x_val = WIDTH - (div * 30); - - for y in 0..HEIGHT { - raster[y * WIDTH + x_val] = GRIDLINE; - } - } -} - -fn draw_line(raster: &mut [u8], values: &[Option], vpp: f32, colour: u8) { - // Draw Line - // this will be discontinuous and i think that's okay. we - // could make it a proper line by keeping track what value - // was last and drawing the whole vertical there - for (x, maybe) in values.iter().enumerate() { - if let Some(value) = maybe { - let value_height = (*value as f32 / vpp) as usize; - if value_height > (HEIGHT - 1) { - //TODO: this causes too-high values not to draw at all; gaps - continue; - } - let y_val = (HEIGHT - 1) - value_height; - - raster[y_val * WIDTH + x] = colour; - } else { - draw_missing(raster, x); - } - } -} - -fn draw_line_continuous(raster: &mut [u8], values: &[Option], vpp: f32, colour: u8) { - let mut last_value = None; - - for (x, maybe) in values.iter().enumerate() { - if let Some(value) = maybe { - let value_height = (*value as f32 / vpp) as usize; - let overflow = value_height > HEIGHT - 1; - let y_val = (HEIGHT - 1) - value_height.min(HEIGHT - 1); - - match last_value { - Some(last) if last > y_val => { - for y_offset in y_val..last { - raster[y_offset * WIDTH + x] = colour; - } - raster[y_val * WIDTH + x] = colour; - } - Some(last) if last < y_val => { - for y_offset in last..y_val { - raster[y_offset * WIDTH + x - 1] = colour; - } - raster[y_val * WIDTH + x] = colour; - } - _ => { - // This is the None case and the equal value case - raster[y_val * WIDTH + x] = colour; - } - } - - last_value = Some(y_val); - } else { - draw_missing(raster, x); - - last_value = None; - } - } -} - -fn draw_line_underfill( - raster: &mut [u8], - values: &[Option], - vpp: f32, - colour: u8, - colour_fill: u8, -) { - for (x, maybe) in values.iter().enumerate() { - if let Some(value) = maybe { - let value_height = (*value as f32 / vpp) as usize; - let overflow = value_height > HEIGHT - 1; - let y_val = (HEIGHT - 1) - value_height.min(HEIGHT - 1); - - for y in y_val..HEIGHT { - raster[y * WIDTH + x] = colour_fill; - } - - if !overflow { - raster[y_val * WIDTH + x] = colour; - } - } else { - draw_missing(raster, x); - } - } -} - -fn draw_missing(raster: &mut [u8], x: usize) { - for y in 0..HEIGHT { - // Makes a dashed line, alternating between the MISSING colour - // and nothing every four pixels - if (y / 4) % 2 == 0 { - raster[y * WIDTH + x] = MISSING; - } - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100755 index 3501e1f..0000000 --- a/src/main.rs +++ /dev/null @@ -1,478 +0,0 @@ -mod atom; -mod db; -mod error; -mod fs; -mod gatherer; -mod griph; -mod markup; -mod settings; -mod templated; -mod timeparse; -mod util; - -use std::{ - fs::File, - io::{BufRead, BufReader, Write}, - os::unix::fs::MetadataExt, - str::FromStr, - sync::{ - atomic::{AtomicU8, AtomicUsize, Ordering}, - Arc, - }, - time::Duration, -}; - -use ::time::OffsetDateTime; -use axum::{ - body::Body, - extract::{Path, State}, - http::{header, StatusCode}, - response::Response, - routing::get, - Extension, Router, -}; -use bempline::{variables, Document, Options}; -use camino::Utf8PathBuf; -use confindent::{Confindent, Node}; -use db::Database; -pub use error::RuntimeError; -use fs::Filesystem; -use gatherer::Gatherer; -use settings::Settings; -use templated::Frontmatter; -use tokio_util::io::ReaderStream; -use tracing_subscriber::{fmt::time, prelude::*, EnvFilter}; -use util::{Referer, RemoteIp, SessionId}; - -use crate::{ - fs::{PathResolution, Webpath}, - markup::Markup, - templated::Templated, -}; - -#[derive(Clone)] -pub struct AwakeState { - pub do_statistics: bool, - pub database: Arc, - pub cache_path: Utf8PathBuf, - /// kbps - pub netinfo_upper_bound: Arc, - /// whole digit % cpu usage - pub cpuinfo_upper_bound: Arc, -} - -#[tokio::main] -async fn main() { - match std::env::args().nth(1).as_deref() { - Some("atomizer") => atom::main(), - /* fallthrough*/ - Some("serve") => (), - _ => (), - } - - tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) - .with( - EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new("info")) - .unwrap(), - ) - .init(); - - let conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap(); - let webroot: Utf8PathBuf = conf.child_parse("Webroot").unwrap(); - let templates = conf.child_value("Templates").unwrap(); - let hostname = conf.child_owned("Hostname").unwrap(); - let dbpath = conf.child_owned("Database").unwrap(); - let cache = conf.child_owned("Cache").unwrap(); - let statistics = match conf.child_value("Statistics") { - None | Some("true") | Some("yes") | Some("collect") => true, - _ => false, - }; - - let database = Database::new(dbpath.into()); - database.create_tables(); - - let fs = Filesystem::new(&webroot); - - let state = AwakeState { - do_statistics: statistics, - database: Arc::new(database), - cache_path: cache.into(), - netinfo_upper_bound: Arc::new(AtomicUsize::new(256)), - cpuinfo_upper_bound: Arc::new(AtomicUsize::new(100)), - }; - - match std::env::args().nth(1).as_deref() { - Some("graph") => { - gatherer::make_mem_graph(&state); - gatherer::make_cpu_graph(&state); - gatherer::make_net_graph(&state); - } - _ => (), - } - - let mut gatherer = Gatherer::new(state.clone()); - gatherer.start(); - - let settings = Settings { - template_dir: Utf8PathBuf::from(webroot.join(templates)) - .canonicalize_utf8() - .unwrap(), - hostname, - }; - - let app = Router::new() - .route("/", get(index_handler)) - .route("/api/stats/:name", get(stats)) - .route("/*path", get(handler)) - .layer(Extension(fs)) - .layer(Extension(settings)) - .with_state(state); - - let listener = tokio::net::TcpListener::bind("0.0.0.0:2560").await.unwrap(); - axum::serve(listener, app).await.unwrap() -} - -async fn index_handler( - state: State, - fse: Extension, - se: Extension, - sid: SessionId, - rfr: Option, -) -> Response { - handler(state, fse, se, sid, rfr, Path(String::from("/"))).await -} - -async fn handler( - State(state): State, - Extension(fs): Extension, - Extension(settings): Extension, - sid: SessionId, - rfr: Option, - Path(path): Path, -) -> Response { - match falible_handler(state, fs, settings, sid, rfr, path).await { - Ok(resp) => resp, - Err(RuntimeError::NotFound { source: _, path }) => { - tracing::warn!("[{sid}] 404 on {path}"); - - Response::builder() - .body(Body::from(format!("the path was not found: {path}"))) - .unwrap() - } - Err(re) => Response::builder() - .body(Body::from(re.to_string())) - .unwrap(), - } -} - -async fn falible_handler( - state: AwakeState, - fs: Filesystem, - settings: Settings, - sid: SessionId, - rfr: Option, - path: String, -) -> Result { - tracing::debug!("webpath = {path}"); - - let webpath: Webpath = path.parse()?; - let resolve = fs.resolve(&webpath)?; - - if !webpath.is_dir() && resolve.is_dirfile { - return Ok(redirect(webpath.as_dir())); - } - - let ext = resolve.filepath.extension().unwrap_or_default(); - - tracing::trace!("[{sid}] referer = {rfr:?}"); - tracing::trace!("[{sid}] webpath = {webpath}"); - - if ext != "html" { - // Logging a non-html asset without a referrer. Let's pretend like - // that means it's the primary resource. - if rfr.is_none() { - tracing::info!("[{sid}] serving {webpath}"); - } - - send_file(resolve.filepath).await - } else { - match rfr { - None => { - tracing::info!("[{sid}] serving {webpath}"); - } - Some(referer) => { - tracing::info!("[{sid}] (refer {referer}) serving {webpath}"); - } - } - - let content = Filesystem::read_to_string(&resolve.filepath).await?; - let result = Templated::from_str(&content); - - match result { - Ok(templated) => send_template(templated, resolve, webpath, state, settings, sid).await, - Err(e) => { - tracing::warn!("error sending template {e}"); - - Ok(Response::builder() - .header(header::CONTENT_TYPE, "text/html") - .body(Body::from(content)) - .unwrap()) - } - } - } -} - -fn redirect>(redirection: S) -> Response { - let location = redirection.into(); - tracing::info!("redirect to {location}"); - Response::builder() - .status(StatusCode::TEMPORARY_REDIRECT) - .header(header::LOCATION, &location) - .body(Body::new(format!("redirecting to {location}"))) - .unwrap() -} - -// 20 megabytes -const STREAM_AFTER: u64 = 20 * 1024 * 1024; - -async fn send_file(filepath: Utf8PathBuf) -> Result { - tracing::debug!(target: "send_file", filepath = ?filepath); - let ext = filepath.extension().unwrap_or_default(); - let stem = filepath.file_stem().unwrap_or_default(); - - let mime = match ext { - // Text - "css" => "text/css", - "html" => "text/html", - "js" => "text/javascript", - "txt" => "text/plain", - "xml" if stem.ends_with("atom") => "application/atom+xml", - "xml" => "application/xml", - - // Multimedia - "gif" => "image/gif", - "jpg" | "jpeg" => "image/jpeg", - "mp4" => "video/mp4", - "png" => "image/png", - _ => "", - }; - tracing::debug!(target: "send_file", mime = ?mime); - - let mut response = Response::builder(); - if !mime.is_empty() { - response = response.header(header::CONTENT_TYPE, mime); - } - - let metadata = Filesystem::metadata(&filepath)?; - if metadata.size() > STREAM_AFTER { - tracing::debug!("large file, streaming to client"); - - let file = Filesystem::open(filepath).await?; - let stream = ReaderStream::new(file); - Ok(response.body(Body::from_stream(stream)).unwrap()) - } else { - tracing::debug!("small file, sending entirely at once"); - - let content = Filesystem::read(filepath).await?; - Ok(response.body(Body::from(content)).unwrap()) - } -} - -async fn send_template( - templated: Templated, - resolve: PathResolution, - webpath: Webpath, - state: AwakeState, - settings: Settings, - sid: SessionId, -) -> Result { - tracing::trace!("[{sid}] sending template"); - let template_stem = templated.frontmatter.get("template").expect("no template"); - let template_name = Utf8PathBuf::from(format!("{template_stem}.html")); - let template_path = settings.template_dir.join(template_name); - - let filename = resolve - .filepath - .file_name() - .expect("template has no filename"); - - let mut template = Document::from_file( - template_path, - Options::default().include_path(bempline::options::IncludeMethod::Path( - settings.template_dir.as_std_path().to_owned(), - )), - ) - .unwrap(); - - let title = templated.frontmatter.get("title").unwrap_or(filename); - - template.set("title", title); - - tracing::trace!("doing opengraph stuff!"); - if let Some(og_description) = templated.frontmatter.get("description") { - let og_title = title; - let og_url = format!("https://{}{}", &settings.hostname, webpath); - - if let Some(art_relpath) = templated.frontmatter.get("art") { - let serving_dir = Utf8PathBuf::from(webpath.first_dir()); - let art_path = serving_dir.join(art_relpath); - - let og_image_alt = match templated.frontmatter.get("art_alt") { - Some(alt) => alt, - None => { - tracing::warn!("[{sid}] {} has art but no alt", resolve.filepath); - "" - } - }; - - let og_image = format!("https://{}/{}", &settings.hostname, art_path); - - variables!(template, og_image, og_image_alt); - } - - let og_site_name = &settings.hostname; - - variables!(template, og_title, og_url, og_description, og_site_name); - } - - tracing::trace!("stylin'!"); - // styles the templated stuff wants - let style_pattern = template.get_pattern("styles").unwrap(); - for style in templated.frontmatter.get_many("style") { - let mut pat = style_pattern.clone(); - pat.set("style", style); - template.set_pattern(pat); - } - - // path to the file for navigation - let mut path: Vec<&str> = webpath.webcanon.iter().collect(); - // we don't want the directory/filename itself - path.pop(); - - if let Some(path_pattern) = template.get_pattern("path") { - let offset = match templated - .frontmatter - .get("path-offset") - .map(|raw| raw.parse::()) - { - Some(Ok(offset)) => offset, - None => 0, - Some(Err(_)) => { - tracing::error!( - "[{sid}] path-offset in template {} is not an integer", - resolve.filepath - ); - - 0 - } - }; - - for _ in 0..offset { - path.pop(); - } - - let mut link = Utf8PathBuf::from("/"); - - let mut pat = path_pattern.clone(); - pat.set("path_link", "/"); - pat.set("path_name", "home"); - template.set_pattern(pat); - - for part in path { - link.push(part); - - let mut pat = path_pattern.clone(); - pat.set("path_link", &link); - pat.set("path_name", part); - template.set_pattern(pat); - } - } - - tracing::trace!("starting published block"); - 'published: { - if let Some(mut published_pattern) = template.get_pattern("published") { - let publish_date_result = templated - .frontmatter - .get("published") - .map(|ts| timeparse::parse(ts)); - - match publish_date_result { - None => break 'published, - Some(Err(_e)) => { - tracing::warn!("template {resolve} has malformed `published` frontmatter"); - break 'published; - } - Some(Ok(datetime)) => { - let published_human = timeparse::format_long(&datetime); - let published_machine = timeparse::iso8601(&datetime); - - variables!(published_pattern, published_human, published_machine); - template.set_pattern(published_pattern); - } - } - } - } - tracing::trace!("finished published block"); - - // insert the page content itself - let markedup = markup::process(&templated.content); - markedup.fill_document(&mut template); - - if templated.frontmatter.get("use-template").is_some() { - let main = template_content(state, &templated.frontmatter, markedup); - template.set("main", main); - } else { - template.set("main", markedup.into_content()); - } - - Ok(Response::builder() - .header(header::CONTENT_TYPE, "text/html") - .body(Body::from(template.compile())) - .unwrap()) -} - -fn template_content(state: AwakeState, frontmatter: &Frontmatter, marked: Markup) -> String { - let mut doc = match marked.into_document() { - Ok(doc) => doc, - Err(content) => return content, - }; - - if frontmatter.get("system-stats").is_some() { - let mem = state.database.get_last_host_meminfo(); - - doc.set("stats.mem.total", mem.total_kb / 1000); - doc.set("stats.mem.usage", mem.usage() / 1000); - - let netinfo_upper = state.netinfo_upper_bound.load(Ordering::Relaxed); - doc.set("stats.net.max_bound", netinfo_upper); - - let cpuinfo_upper = state.cpuinfo_upper_bound.load(Ordering::Relaxed); - doc.set("stats.cpu.max_bound", cpuinfo_upper); - } - - doc.compile() -} - -async fn stats(State(state): State, Path(name): Path) -> Response { - const NAMES: &[&'static str] = &[ - "current_hostmeminfo.gif", - "current_hostnetinfo.gif", - "current_hostcpuinfo.gif", - ]; - - if NAMES.contains(&name.as_str()) { - let path = state.cache_path.join(name); - match send_file(path).await { - Ok(resp) => resp, - Err(e) => Response::builder().body(Body::from(e.to_string())).unwrap(), - } - } else { - tracing::debug!("invalid stat requested: {name}"); - - Response::builder() - .body(Body::from("that stat is not real")) - .unwrap() - } -} diff --git a/src/markup.rs b/src/markup.rs deleted file mode 100755 index bde28ee..0000000 --- a/src/markup.rs +++ /dev/null @@ -1,409 +0,0 @@ -use std::{borrow::Cow, ops::Deref}; - -use bempline::{Document, Options}; - -struct State { - active_id: Option, - active_fill: Option, - paragraphs: bool, - - processed: String, - current: String, - fills: Vec, - escaped_html: bool, - last_blank: bool, -} - -impl Default for State { - fn default() -> Self { - Self { - active_id: None, - active_fill: None, - paragraphs: true, - - processed: String::new(), - current: String::new(), - fills: vec![], - escaped_html: false, - last_blank: true, - } - } -} - -impl State { - /// Get an opening paragraph tag with any attributes currently set. - fn get_open_paragraph(&mut self) -> String { - match self.active_id.take() { - None => String::from("

"), - Some(id) => format!(r#"

"#), - } - } - - fn paragraph(&mut self) { - let current = self.take_current(); - let content = self.make_paragraph(current); - - if let Some(fill) = self.active_fill.as_mut() { - // linebreak if there is already text pushed to fill.content - if !fill.content.is_empty() { - fill.content.push('\n'); - } - - fill.content.push_str(&content); - } else { - // linebreak if there is already text pushed to final - if !self.processed.is_empty() { - self.processed.push('\n'); - } - - self.processed.push_str(&content); - } - } - - fn make_paragraph(&mut self, content: String) -> String { - if self.should_paragraph_string(&content) { - let open = self.get_open_paragraph(); - format!("{open}\n{}\n

", content) - } else { - content - } - } - - fn should_paragraph_string(&self, string: &str) -> bool { - // wrap paragraphs if all of these are true: - // - we're supposed to be wrapping paragraphs - // - either of these is true: - // - the line does not start with < - // OR - // - the line starts with < AND it's been escaped - self.paragraphs - && (!string.starts_with('<') || (string.starts_with('<') && self.escaped_html)) - } - - pub fn process_line(&mut self, line: &str) { - if self.parse_command(line) { - // Don't set last_blank; a command does not break up a text block - return; - } - - if !line.is_empty() { - self.push_current(line); - - self.last_blank = false; - } else { - if !self.paragraphs { - // It's a blank line, but we're not doing paragraphs, - // so leave it in the document - self.current.push('\n'); - self.last_blank = false; - return; - } - - // line is empty, close the textblock - self.finish_current(); - } - } - - pub fn done(mut self) -> Markup { - self.finish_current(); - self.finish_active_fill(); - - Markup { - content: self.processed, - fill: self.fills, - } - } - - fn escape_line<'a>(&mut self, line: &'a str) -> &'a str { - if let Some(strip) = line.strip_prefix('\\') { - match strip.chars().next() { - Some('[') => strip, - Some('<') => { - if self.last_blank { - self.escaped_html = true; - } - - strip - } - Some('\\') => strip, - _ => line, - } - } else { - line - } - } - - /// Possibly parses a line as a command and mutates internal state. - /// # Returns - /// true if the line was a command, false otherwise. - fn parse_command(&mut self, line: &str) -> bool { - match line.strip_prefix('[') { - Some(line) => match line.strip_suffix(']') { - Some(cmd) => self.run_command(cmd), - None => false, - }, - None => false, - } - } - - fn run_command(&mut self, cmd: &str) -> bool { - match cmd.trim() { - "@paragraphs off" => { - self.finish_current(); - self.paragraphs = false; - true - } - "@paragraphs on" => { - self.finish_current(); - self.paragraphs = true; - true - } - "@fill-end" => { - self.finish_current(); - self.finish_active_fill(); - true - } - template_key if cmd.starts_with("@fill ") => { - self.finish_current(); - - let key = template_key.strip_prefix("@fill ").unwrap().to_owned(); - self.active_fill = Some(FillComand { - paragraphs: self.paragraphs, - key, - content: String::new(), - }); - self.paragraphs = false; - - true - } - annotation if cmd.starts_with('#') => { - self.active_id = Some(annotation[1..].to_owned()); - true - } - _ => false, - } - } - - fn take_current(&mut self) -> String { - self.current.split_off(0) - } - - fn push_current(&mut self, text: &str) { - if !self.current.is_empty() { - self.current.push('\n'); - } - - let escaped = self.escape_line(text); - self.current.push_str(escaped); - } - - fn finish_current(&mut self) { - if !self.current.is_empty() { - // Calling paragraph clears self.current - self.paragraph(); - - // reset block dependant state - self.last_blank = true; - self.escaped_html = false; - } - } - - fn finish_active_fill(&mut self) { - if let Some(fill) = self.active_fill.take() { - self.fills.push(Fill { - key: fill.key, - content: fill.content, - }); - - self.paragraphs = fill.paragraphs; - } - } -} - -pub struct Markup { - content: String, - fill: Vec, -} - -impl Markup { - pub fn fill(&self) -> &[Fill] { - &self.fill - } - - pub fn content(&self) -> &str { - &self.content - } - - pub fn into_content(self) -> String { - self.content - } - - pub fn into_document(self) -> Result { - let Ok(mut doc) = Document::from_str(&self.content, Options::default()) else { - return Err(self.content); - }; - - self.fill_document(&mut doc); - - Ok(doc) - } - - pub fn fill_document(&self, doc: &mut Document) { - for fill in &self.fill { - doc.set(&fill.key, &fill.content); - } - } -} - -impl Deref for Markup { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.content - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Fill { - pub key: String, - pub content: String, -} - -pub fn process(raw: &str) -> Markup { - tracing::trace!("processing!"); - let mut state = State::default(); - - for line in raw.lines() { - println!("raw line: '{line}'"); - state.process_line(line) - } - - state.done() -} - -struct FillComand { - /// Storage for the previous paragraphs variable, as @fill unsets it - paragraphs: bool, - - key: String, - content: String, -} - -#[cfg(test)] -mod test { - use crate::markup::{process, Fill}; - - #[test] - fn parses_no_commands() { - let blk1 = "line one\nline two"; - let blk2 = "block two"; - - assert_eq!(*process(blk1), format!("

\n{blk1}\n

")); - - let tst = format!("{blk1}\n\n{blk2}"); - assert_eq!( - *process(&tst), - format!("

\n{blk1}\n

\n

\n{blk2}\n

") - ) - } - - #[test] - fn parses_paragraph_off() { - let str = "[@paragraphs off]\none two\n\nthree\nfour"; - assert_eq!(*process(str), "one two\n\nthree\nfour") - } - - #[test] - fn parses_adds_annotation() { - let str = "[#greeting]\nHello!"; - assert_eq!(*process(str), "

\nHello!\n

") - } - - #[test] - fn doesnt_wrap_html() { - let str = "hello!\n\nhi, how are you?"; - assert_eq!(*process(str), "

\nhello!\n

\nhi, how are you?") - } - - #[test] - fn correctly_escapes() { - let str = "\\[@paragraph on]\n\\Hello!\n\\\\Goodbye!"; - let correct = "

\n[@paragraph on]\nHello!\n\\Goodbye!\n

"; - assert_eq!(*process(str), correct) - } - - #[test] - fn wraps_escaped_html() { - let str = "\\test"; - let correct = "

\ntest\n

"; - assert_eq!(*process(str), correct) - } - - #[test] - fn parses_extracts_fill() { - let str = "[@fill key]\nfilled stuff!\n[@fill-end]\nmeow"; - let correct = "

\nmeow\n

"; - let marked = process(str); - assert_eq!( - marked.fill, - vec![Fill { - key: "key".into(), - content: "filled stuff!".into() - }] - ); - assert_eq!(marked.content, correct) - } - - const BASE: &str = "test/markup"; - fn test_files(test: &str) { - let input_path = format!("{BASE}/{test}/input.html"); - let output_path = format!("{BASE}/{test}/output.html"); - - let input = std::fs::read_to_string(input_path).unwrap(); - let output = std::fs::read_to_string(output_path).unwrap(); - - assert_eq!(*process(&input), output) - } - - #[test] - fn parses_onoff() { - test_files("paragraph toggle") - } -} - -#[cfg(test)] -mod document_test { - use crate::markup::process; - - const BASE: &str = "test/markup"; - fn test_files(test: &str) { - let input_path = format!("{BASE}/{test}/input.html"); - let output_path = format!("{BASE}/{test}/output.html"); - - let input = std::fs::read_to_string(input_path).unwrap(); - let output = std::fs::read_to_string(output_path).unwrap(); - - let actual = process(&input).into_document().unwrap().compile(); - - assert_eq!(actual, output) - } - - #[test] - fn inserts_fill() { - let input = "[@fill main]\nwords!\n[@fill-end]\n{main}"; - let expected = "

\nwords!\n

"; - let markup = process(input); - let doc = markup.into_document().unwrap().compile(); - assert_eq!(doc, expected) - } - - #[test] - fn fill_restores_paragraphs() { - test_files("fill paragraphs"); - } - - #[test] - fn parses_complicated_fill() { - test_files("complicated fill"); - } -} diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100755 index 06abc18..0000000 --- a/src/settings.rs +++ /dev/null @@ -1,7 +0,0 @@ -use camino::Utf8PathBuf; - -#[derive(Clone, Debug)] -pub struct Settings { - pub template_dir: Utf8PathBuf, - pub hostname: String, -} diff --git a/src/templated.rs b/src/templated.rs deleted file mode 100755 index 4598ecc..0000000 --- a/src/templated.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::str::FromStr; - -/// The content part of a file that fills in a template. -pub struct Templated { - pub frontmatter: Frontmatter, - pub content: String, -} - -impl FromStr for Templated { - type Err = TemplateError; - - fn from_str(raw: &str) -> Result { - tracing::trace!("parsing Templated"); - - let (front, content) = match raw.strip_prefix("---\n") { - None => return Err(TemplateError::MissingFrontmatter), - Some(no_start) => match no_start.split_once("\n---\n") { - None => return Err(TemplateError::MissingFrontmatter), - Some((front, content)) => (front, content), - }, - }; - - tracing::trace!("isolated frontmatter"); - - Ok(Self { - frontmatter: front.parse()?, - content: content.to_owned(), - }) - } -} - -pub struct Frontmatter { - entries: Vec<(String, String)>, -} - -impl FromStr for Frontmatter { - type Err = TemplateError; - - fn from_str(raw: &str) -> Result { - let mut entries = vec![]; - - for line in raw.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with('#') { - // Skip over blank lines and comments - continue; - } - - let (key, value) = match trimmed.split_once('=') { - None => { - return Err(TemplateError::MalformedFrontmatterEntry { - line: trimmed.to_owned(), - }); - } - Some(tup) => tup, - }; - - entries.push((key.trim().to_owned(), value.trim().to_owned())); - tracing::trace!("pushed frontmatter entry {} successfully", key.trim()); - } - - Ok(Self { entries }) - } -} - -impl Frontmatter { - pub fn get(&self, key: &str) -> Option<&str> { - self.entries - .iter() - .find(|(k, _)| k == key) - .map(|(_, v)| v.as_str()) - } - - pub fn get_many(&self, key: &str) -> Vec<&str> { - // this could probably be a filter_map() but the explicit filter and - // then map seems cleaner - self.entries - .iter() - .filter(|(k, _)| k == key) - .map(|(_, v)| v.as_str()) - .collect() - } -} - -#[derive(Debug, snafu::Snafu)] -pub enum TemplateError { - #[snafu(display("file is missing the frontmatter"))] - MissingFrontmatter, - #[snafu(display("the frontmatter entry is malformed: {line}"))] - MalformedFrontmatterEntry { line: String }, -} - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use crate::templated::{Frontmatter, Templated}; - - #[test] - fn frontmatter_parse() { - let frntmtr = "key=value\nkey2=value2"; - assert!(Frontmatter::from_str(frntmtr).is_ok()) - } - - #[test] - fn frontmatter_fails_on_invalid_line() { - let front = "key1=value1\nincorrect line"; - assert!(Frontmatter::from_str(front).is_err()) - } - - #[test] - fn templated_parse() { - let tempalted = "---\ntitle=Title!\n---\nContent line!"; - assert!(Templated::from_str(tempalted).is_ok()) - } - - #[test] - fn templated_doesnt_eat_frontmatter_line() { - let templated = "---\ntitle=Title---\nContent"; - assert!(Templated::from_str(templated).is_err()); - } - - #[test] - fn templated_parses_correct() { - let raw = "---\ntitle=Title!\n---\n

Paragraph!

"; - let tmpl = Templated::from_str(raw).unwrap(); - assert_eq!(tmpl.frontmatter.get("title").unwrap(), "Title!"); - assert_eq!(tmpl.content, "

Paragraph!

") - } -} diff --git a/src/timeparse.rs b/src/timeparse.rs deleted file mode 100755 index fe8ca60..0000000 --- a/src/timeparse.rs +++ /dev/null @@ -1,216 +0,0 @@ -use time::{ - error::{Parse, TryFromParsed}, - format_description::{well_known, FormatItem}, - macros::{format_description, offset, time}, - Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday, -}; - -const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]"); - -const DATE: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); -const TIME_24HOUR: &[FormatItem<'_>] = format_description!("[hour padding:zero]:[minute]"); -const TIME_12HOUR: &[FormatItem<'_>] = - format_description!("[hour padding:none]:[minute][period case:lower case_sensitive:false]"); - -#[allow(unused)] -const OFFSET: &[FormatItem<'_>] = - format_description!("[offset_hour sign:mandatory]:[offset_minute]"); - -const CST: UtcOffset = offset!(-06:00); -const CDT: UtcOffset = offset!(-05:00); - -// Ripped from infica with the month of Sol removed. I just want the 3 letter months -/// Capitalized months (long, short) and lowercase months (long, short). -// it seems useful to have the lowercase here so we don't have to always call -// to_lowercase -const MONTHS: [[&str; 4]; 12] = [ - ["January", "Jan", "january", "jan"], - ["February", "Feb", "february", "feb"], - ["March", "Mar", "march", "mar"], - ["April", "Apr", "april", "apr"], - ["May", "May", "may", "may"], - ["June", "Jun", "june", "jun"], - ["July", "Jul", "july", "jul"], - ["August", "Aug", "august", "aug"], - ["September", "Sep", "september", "sep"], - ["October", "Oct", "october", "oct"], - ["November", "Nov", "november", "nov"], - ["December", "Dec", "december", "dec"], -]; - -/// Offset for the united states in Central Time. Accounts for DST -/// DST starts the 2nd sunday in March and ends the 1st sunday in November. -/// https://www.nist.gov/pml/time-and-frequency-division/popular-links/daylight-saving-time-dst -fn us_dst_central_offset(datetime: PrimitiveDateTime) -> UtcOffset { - let second_sunday_march = { - let mut seen_sunday = false; - let mut curr = Date::from_calendar_date(datetime.year(), time::Month::March, 1).unwrap(); - - loop { - if curr.weekday() == Weekday::Sunday { - if seen_sunday { - break PrimitiveDateTime::new(curr, time!(02:00 AM)); - } else { - seen_sunday = true; - } - } - - curr = curr.next_day().unwrap(); - } - }; - - let first_sunday_november = { - let mut curr = Date::from_calendar_date(datetime.year(), time::Month::November, 1).unwrap(); - - loop { - if curr.weekday() == Weekday::Sunday { - break PrimitiveDateTime::new(curr, time!(02:00 AM)); - } else { - curr = curr.next_day().unwrap(); - } - } - }; - - if datetime >= second_sunday_march && datetime < first_sunday_november { - CDT - } else { - CST - } -} - -fn parse_offset(raw: &str) -> UtcOffset { - match raw.to_ascii_uppercase().as_str() { - "CST" => CST, - "CDT" => CDT, - _ => unimplemented!(), - } -} - -fn parse_time(raw: &str) -> Result { - match Time::parse(raw, TIME_24HOUR) { - Err(_e) => Time::parse(raw, TIME_12HOUR), - Ok(t) => Ok(t), - } -} - -fn parse_date(raw: &str) -> Result { - Date::parse(raw, DATE) -} - -/// Parses an OffsetDateTime from any of the following formats: -/// - date only: 2024-04-13 -/// - date and time: 2024-04-13 2:56 -/// - dat, time, and offset: 2024-04-13 2:56 CDT -/// -/// If a time is not procided, noon (12:00) is assumed. -/// -/// If an offset is not provided it will be assumed to be US Central Time and -/// it will correct for daylight savings. -pub fn parse(raw: &str) -> Result { - let mut splits = raw.trim().split(' '); - - let date = match splits.next() { - None => return Err(Parse::TryFromParsed(TryFromParsed::InsufficientInformation)), - Some(raw) => parse_date(raw)?, - }; - - let time = match splits.next() { - None => time!(12:00:00), - Some(raw) => parse_time(raw)?, - }; - - let calculated_offset = us_dst_central_offset(PrimitiveDateTime::new(date, time)); - let offset = match splits.next() { - None => calculated_offset, - Some(raw) => { - let offset = parse_offset(raw); - if offset != calculated_offset { - //FIXME: gen 2024-12; warn here but format it rightly. - () - } - - offset - } - }; - - Ok(OffsetDateTime::new_in_offset(date, time, offset)) -} - -pub fn format_long(datetime: &OffsetDateTime) -> String { - let year = datetime.year(); - let month = MONTHS[datetime.month() as usize - 1][1]; - let day = datetime.day(); - let weekday = datetime.weekday(); - - format!("{weekday} {day}, {month} {year}") -} - -pub fn iso8601(datetime: &OffsetDateTime) -> String { - datetime - .format(&well_known::Iso8601::DATE_TIME_OFFSET) - .unwrap() -} - -#[cfg(test)] -mod test { - use time::{ - macros::{datetime, offset}, - OffsetDateTime, PrimitiveDateTime, - }; - - use crate::timeparse::{parse_offset, us_dst_central_offset, CDT, CST, FMT}; - - #[test] - fn calculates_cst_cdt_correctly() { - fn daylight(raw: &str) { - let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); - - if us_dst_central_offset(date) != CDT { - panic!("CDT failed on {raw}") - } - } - - fn standard(raw: &str) { - let date = PrimitiveDateTime::parse(raw, FMT).unwrap(); - - if us_dst_central_offset(date) != CST { - panic!("CST failed on {raw}") - } - } - - // make sure CST and CDT are what we expect - assert_eq!(CST, offset!(-06:00)); - assert_eq!(CDT, offset!(-05:00)); - - daylight("2024-04-13 02:56"); - daylight("2024-03-10 14:00"); - daylight("2024-03-10 02:00"); - - standard("2024-12-01 00:00"); - standard("2024-11-03 14:00"); - standard("2024-11-03 02:00"); - - // other years - daylight("2023-03-12 12:00"); // DST start Mar 12, end Nov 5 - standard("2023-03-11 23:00"); - - standard("2023-11-05 12:00"); - daylight("2023-11-04 23:00"); - } - - #[test] - fn paress_timezone_code() { - assert_eq!(parse_offset("CST"), CST); - assert_eq!(parse_offset("CDT"), CDT); - } - - fn test_parse(raw: &str, expected: OffsetDateTime) { - assert_eq!(super::parse(raw).unwrap(), expected) - } - - #[test] - fn parse_date_only() { - test_parse("2024-04-13", datetime!(2024-04-13 12:00 -05:00)); - test_parse("2024-01-01", datetime!(2024-01-01 12:00 -06:00)); - } -} diff --git a/src/util.rs b/src/util.rs deleted file mode 100755 index a3cc064..0000000 --- a/src/util.rs +++ /dev/null @@ -1,137 +0,0 @@ -use core::fmt; -use std::ops::Deref; - -use axum::{ - async_trait, - extract::FromRequestParts, - http::{header, request::Parts, HeaderName, StatusCode}, - RequestPartsExt, -}; -use sha2::{Digest, Sha256}; -use time::{format_description::FormatItem, macros::format_description, OffsetDateTime}; - -#[derive(Debug, Clone)] -pub struct RemoteIp(String); - -#[async_trait] -impl FromRequestParts for RemoteIp -where - S: Send + Sync, -{ - type Rejection = (StatusCode, &'static str); - - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - if let Some(forwarded_for) = parts - .headers - .get(HeaderName::from_static("x-forwarded-for")) - { - match forwarded_for.to_str() { - Err(_e) => Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")), - Ok(remoteip) => Ok(Self(remoteip.to_owned())), - } - } else { - Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")) - } - } -} - -impl Deref for RemoteIp { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); - -#[derive(Debug, Copy, Clone)] -pub struct SessionId([u8; 4]); - -#[async_trait] -impl FromRequestParts for SessionId -where - S: Send + Sync, -{ - type Rejection = (StatusCode, &'static str); - - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - let remote = parts - .extract::() - .await - .map(|rip| rip.0) - .unwrap_or(String::from("127.0.0.1")); - - let agent = parts - .headers - .get(header::USER_AGENT) - .and_then(|ua| ua.to_str().ok()) - .unwrap_or("unknown"); - - let date = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); - - let string = format!("{remote} - {agent} - {}", date.format(FMT).unwrap()); - - let mut hasher = Sha256::new(); - hasher.update(string.as_bytes()); - let hash = hasher.finalize(); - let slc: [u8; 4] = hash.as_slice()[0..4].try_into().unwrap(); - - Ok(Self(slc)) - } -} - -impl fmt::Display for SessionId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in self.0.as_slice() { - write!(f, "{byte:02X}")?; - } - - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct Referer(String); - -#[async_trait] -impl FromRequestParts for Referer -where - S: Send + Sync, -{ - type Rejection = (StatusCode, &'static str); - - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - if let Some(refer) = parts.headers.get(header::REFERER) { - match refer.to_str() { - Err(_e) => Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")), - Ok(refer) => match url::Url::parse(refer) { - Err(_e) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - "failed to parse referer url", - )), - Ok(url) => match url.host_str() { - None => Err((StatusCode::INTERNAL_SERVER_ERROR, "referer url had no host")), - Some(str) => Ok(Referer(str.to_owned())), - }, - }, - } - } else { - Err((StatusCode::INTERNAL_SERVER_ERROR, "server error")) - } - } -} - -impl Deref for Referer { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for Referer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} -- cgit 1.4.1-3-g733a5