commit f67dcd8f2a86c44ce01ee6fe0ee394b52e7ebb98 Author: Aleksandr Date: Sun Nov 17 14:58:34 2024 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5ff07f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target + + +# Added by cargo +# +# already existing elements were commented out + +#/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3c05fb7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1486 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49c41b948da08fb481a94546cd874843adc1142278b0af4badf9b1b78599d68d" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "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 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[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 = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[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 = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +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 = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jiff" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d9d414fc817d3e3d62b2598616733f76c4cc74fbac96069674739b881295c8" +dependencies = [ + "jiff-tzdb-platform", + "serde", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] + +[[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.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[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.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[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 = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[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.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +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_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "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.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[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 = "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 = "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.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "url" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vnj" +version = "0.1.0" +dependencies = [ + "axum", + "clap", + "color-eyre", + "eyre", + "jiff", + "parking_lot", + "serde", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +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 new file mode 100644 index 0000000..d6d14e7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "vnj" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "vnj" +path = "bin/main.rs" + +[lib] +name = "vnj" +path = "src/lib.rs" + +[dependencies] +axum = { version = "0.7.8", features = ["macros", "multipart"] } +clap = { version = "4.5.21", features = ["derive"] } +color-eyre = "0.6.3" +eyre = "0.6.12" +jiff = { version = "0.1.14", features = ["serde"] } +parking_lot = "0.12.3" +serde = { version = "1.0.215", features = ["derive"] } +tokio = { version = "1.41.1", features = ["rt", "rt-multi-thread", "net", "sync", "macros", "time", "parking_lot", "signal"] } +toml = "0.8.19" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +url = { version = "2.5.3", features = ["serde"] } +uuid = { version = "1.11.0", features = ["v4"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f65b67 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# vnj + +VN journal and post scheduling. + +This thing is made in a couple of hours and its throwaway, so the code is real shit. + +# Use-cases + +Main use-case is to keep what we post in the channel locally, so we could fill upcoming site with content faster, additionally helps with scheduling. + +# Development & Deployment + +Wanna contribute? There's `flake.nix` file to set up development environment: + +```shell +$ nix develop +``` + +Wanna deploy yourself? I made the nixos module for that: + +```nix +let + mkCfg = port: { + app = { + secret.path = "path/to/secret"; + journal = "path/to/journal"; + }; + + http = { + listen = "0.0.0.0:${port}" + }; + }; +in +{ + services.vnj = { + enable = true; + + instances.en = mkCfg 8081; + instances.ru = mkCfg 8082; + }; +} +``` + +Enjoy. + diff --git a/bin/main.rs b/bin/main.rs new file mode 100644 index 0000000..9e183ec --- /dev/null +++ b/bin/main.rs @@ -0,0 +1,79 @@ +use std::{ + fs, + future::IntoFuture, + path::{Path, PathBuf}, +}; + +use clap::Parser; + +use eyre::Context; +use tokio::{net::TcpListener, runtime as rt}; +use tracing_subscriber::FmtSubscriber; + +use vnj::{app::App, cfg, db::Db, notifies, state::State}; + +#[derive(Debug, Parser)] +pub struct Args { + /// Path to the TOML config. + #[clap(long, short)] + pub config: PathBuf, +} + +async fn run(cfg: cfg::Root) -> eyre::Result<()> { + let listener = TcpListener::bind(cfg.http.listen) + .await + .wrap_err("failed to bind HTTP port")?; + + let db = Db::from_root(&cfg.app.journal)?; + let notifies = notifies::make(); + let app = App::new(db, notifies.clone())?; + let state = State::new(app, cfg)?; + + let daemon = tokio::spawn(vnj::daemon::run(state.clone(), notifies.clone())); + let router = vnj::http::make(state.clone()).with_state(state); + + tracing::info!("listening on {}", listener.local_addr()?); + let server = tokio::spawn(axum::serve(listener, router).into_future()); + + tokio::try_join!(daemon, server) + .wrap_err("error while running")? + .0?; + + Ok(()) +} + +fn read_config(path: &Path) -> eyre::Result { + let contents = fs::read_to_string(path).wrap_err("failed to read")?; + let cfg: cfg::Root = toml::from_str(&contents).wrap_err("failed to parse TOML")?; + + Ok(cfg) +} + +fn setup_logger(level: cfg::LogLevel) -> eyre::Result<()> { + let level: Option = level.into(); + let subscriber = FmtSubscriber::builder(); + let subscriber = if let Some(level) = level { + subscriber.with_max_level(level) + } else { + subscriber.with_max_level(tracing::level_filters::LevelFilter::OFF) + }; + let subscriber = subscriber.finish(); + + tracing::subscriber::set_global_default(subscriber)?; + + Ok(()) +} + +fn main() -> eyre::Result<()> { + let rt = rt::Builder::new_current_thread() + .enable_all() + .thread_name("vnj") + .build() + .wrap_err("failed to create tokio runtime")?; + let args = Args::parse(); + let cfg = read_config(&args.config).wrap_err("failed to load the config")?; + + setup_logger(cfg.app.log_level)?; + + rt.block_on(run(cfg)) +} diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..1b5073e --- /dev/null +++ b/config.toml @@ -0,0 +1,10 @@ +[app] +secret = "1337" + +log_level = "debug" +journal = "./journal" + +[http] +listen = "0.0.0.0:8089" + + diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0420197 --- /dev/null +++ b/flake.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "naersk": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1721727458, + "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", + "owner": "nix-community", + "repo": "naersk", + "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1731676054, + "narHash": "sha256-OZiZ3m8SCMfh3B6bfGC/Bm4x3qc1m2SVEAlkV6iY7Yg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5e4fbfb6b3de1aa2872b76d49fafc942626e2add", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "naersk": "naersk", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1731820690, + "narHash": "sha256-/hHFMTD+FGURXZ4JtfXoIgpy87zL505pVi6AL76Wc+U=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "bbab2ab9e1932133b1996baa1dc00fefe924ca81", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..8c56cc2 --- /dev/null +++ b/flake.nix @@ -0,0 +1,96 @@ +{ + description = "vnj"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + naersk = { + url = "github:nix-community/naersk"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + naersk, + nixpkgs, + flake-utils, + rust-overlay, + }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [(import rust-overlay)]; + pkgs = import nixpkgs { inherit system overlays; }; + + rust = pkgs.rust-bin.stable.latest.default; + naersk' = pkgs.callPackages naersk {}; + + vnj = naersk'.buildPackage { + src = ./.; + }; + + nixosModule = { pkgs, config, lib, ... }: + let + ts = lib.types; + cfg = config.services.vnj; + in + { + options.services.vnj = { + enable = lib.mkEnableOption "vnj"; + user = lib.mkOption { + type = ts.str; + }; + + instances = lib.mkOption { + description = "instances of the vnj"; + type = ts.attrsOf ts.attrs; + }; + }; + + config.systemd.services = lib.mkIf cfg.enable (lib.attrsets.mapAttrs' (name: value: + let + gen = pkgs.formats.toml {}; + toml-cfg = gen.generate "config.toml" value; + in + { + name = "vnj-${name}"; + value = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + description = "VNJ"; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + ExecStart = "${vnj}/bin/vnj --config ${toml-cfg}"; + }; + }; + } + ) cfg.instances); + }; + in + { + packages.vnj = vnj; + packages.default = vnj; + + nixosModules.default = nixosModule; + + devShells.default = pkgs.mkShell { + shellHook = '' + export PS1="(vnj) $PS1" + ''; + + buildInputs = [ + (rust.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }) + ]; + }; + } + ); +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..47d50d8 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,141 @@ +use std::{ + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; + +use crate::{ + db::{self, UploadId, UploadKind}, + notifies::Notifies, + post_queue, + schemas::novel::Novel, + schemas::sanity::ensure_slug, + subs, +}; + +#[derive(Debug)] +pub struct App { + db: db::Db, + + notifies: Arc, + + pub subs: subs::Subs, + pub queue: post_queue::Queue, +} + +fn measure(f: impl FnOnce() -> O) -> (O, Duration) { + let start = Instant::now(); + (f(), start.elapsed()) +} + +impl App { + pub fn begin_upload(&self, slug: &str, kind: UploadKind) -> eyre::Result<(UploadId, PathBuf)> { + ensure_slug(slug)?; + let entry = self.db.entry(slug); + Ok(entry.start_upload(slug, kind)) + } + + pub fn finish_upload(&self, slug: &str, id: UploadId, file_name: &str) -> eyre::Result<()> { + ensure_slug(slug)?; + let entry = self.db.entry(slug); + entry.finish_upload(id, file_name)?; + + Ok(()) + } +} + +impl App { + pub fn novel(&self, slug: &str) -> eyre::Result { + ensure_slug(slug)?; + Ok(self.db.entry(slug)) + } + + pub fn enumerate_novels(&self) -> Vec<(String, db::Entry)> { + self.db.enumerate() + } + + pub fn add_novel(&mut self, slug: impl Into, novel: Novel) -> eyre::Result<()> { + let slug = slug.into(); + ensure_slug(&slug)?; + let db_entry = self.db.entry(&slug); + db_entry.init(&novel)?; + + self.queue.add(post_queue::Item { + novel: slug, + post_at: novel.post_at, + }); + + // While storing permit seems unnecessary, but actually + // it prevents race condition, so keep it. + self.notifies.update.notify_one(); + + Ok(()) + } +} + +impl App { + pub fn mark_posted_until(&mut self, ts: jiff::Zoned) { + self.db.update_state(move |s| { + s.posted_until = Some(ts); + }); + } + + pub fn reload_queue(&mut self) -> eyre::Result<()> { + tracing::info!("loading full database..."); + let old_entries = self.queue.len(); + + let (queue, dur) = measure(|| { + let queue: post_queue::Queue = self + .db + .enumerate() + .into_iter() + .filter_map(|(slug, e)| { + let result = e.read_info(); + + match result { + Ok(n) => { + if let Some(ref posted_until) = self.db.state().posted_until { + if &n.post_at <= posted_until { + return None; + } + } + + Some(Ok(post_queue::Item { + novel: slug, + post_at: n.post_at, + })) + } + + Err(e) => Some(Err(e)), + } + }) + .collect::>()?; + + Ok::<_, eyre::Report>(queue) + }); + + self.queue = queue?; + tracing::info!( + took = ?dur, + old = old_entries, + new = self.queue.len(), + "loaded database and created queue", + ); + self.notifies.update.notify_one(); + + Ok(()) + } + + pub fn new(db: db::Db, notifies: Arc) -> eyre::Result { + let queue = post_queue::Queue::default(); + let mut this = Self { + db, + queue, + subs: subs::Subs::new(Arc::clone(¬ifies)), + notifies, + }; + this.reload_queue()?; + + Ok(this) + } +} diff --git a/src/cfg.rs b/src/cfg.rs new file mode 100644 index 0000000..580c472 --- /dev/null +++ b/src/cfg.rs @@ -0,0 +1,65 @@ +use std::{fs, net::SocketAddr, path::PathBuf}; + +use eyre::Context; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Http { + pub listen: SocketAddr, +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum LogLevel { + Info, + Debug, + // Error, // there's no error logs. + Off, +} + +impl From for Option { + fn from(value: LogLevel) -> Self { + use tracing::Level as L; + + Some(match value { + LogLevel::Info => L::INFO, + LogLevel::Debug => L::DEBUG, + LogLevel::Off => return None, + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(untagged, rename_all = "snake_case")] +pub enum Secret { + Path { path: PathBuf }, + Plain(String), +} + +impl Secret { + pub fn into_string(&self) -> eyre::Result { + match self { + Self::Path { path } => fs::read_to_string(path).wrap_err("failed to read secret"), + Self::Plain(s) => Ok(s.clone()), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct App { + pub secret: Secret, + + pub log_level: LogLevel, + pub journal: PathBuf, +} + +#[derive(Debug, Deserialize)] +pub struct Journal { + pub root: PathBuf, +} + +#[derive(Debug, Deserialize)] +pub struct Root { + pub app: App, + pub http: Http, +} diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..53d9d04 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,124 @@ +use std::{ + future::Future, + sync::Arc, + time::{Duration, Instant}, +}; + +use tokio::{ + signal::unix::{signal, SignalKind}, + time, +}; + +use crate::notifies::Notifies; +use crate::state::State; + +async fn sleep_or_hang(deadline: Option) { + if let Some(deadline) = deadline { + time::sleep_until(deadline.into()).await + } else { + // Never wake-up. + struct Never; + + impl Future for Never { + type Output = (); + + fn poll( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + std::task::Poll::Pending + } + } + + Never.await + } +} + +pub async fn run(state: State, notifies: Arc) -> eyre::Result<()> { + tracing::info!("started daemon"); + + let mut on_disk_update = signal(SignalKind::hangup()).unwrap(); + + loop { + let closest_wake_up = 'w: { + let app = state.app.read(); + if !app.subs.has_listeners() { + tracing::info!("the queue has no listeners, so it's pointless to schedule wake-up"); + break 'w None; + } + + if let Some(entry) = app.queue.closest() { + let now = jiff::Zoned::now(); + let sleep_nanos = now.duration_until(&entry.post_at).as_nanos().max(0) as u64; + + Some(Duration::from_nanos(sleep_nanos)) + } else { + None + } + } + .map(|dur| { + tracing::info!(after = ?dur, "the next wake-up is scheduled"); + Instant::now() + dur + }); + + tokio::select! { + biased; + + _ = on_disk_update.recv() => { + tracing::info!("got sighup, reloading queue..."); + let mut app = state.app.write(); + app.reload_queue()?; + } + + _reload = notifies.update.notified() => { + tracing::info!("the database was updated, woke up"); + continue; + } + + _new_sub = notifies.new_subscriber.notified() => { + tracing::debug!("got new subscriber"); + + continue; + } + + _woke_for_update = sleep_or_hang(closest_wake_up) => { + let mut app = state.app.write(); + let app = &mut *app; + + let Some(item) = app.queue.closest_mut() else { + no_updates(); + continue; + }; + + // First we need to get our waiters. + if !app.subs.has_listeners() { + // If no one is interested in update, then we + // don't consume it. + tracing::warn!("got no subscribers for the update"); + continue; + } + + // And then pop update out of the queue. This is critical, + // since we simply eat update without notifying anyone. + let now = jiff::Zoned::now(); + let Some(upd) = item.try_pop(&now) else { + no_updates(); + continue; + }; + let pending_subs = app.subs.consume(); + + for sub in pending_subs { + if let Err(e) = sub.send(upd.novel.clone()) { + tracing::warn!(due_to = e, "one subscriber lost the update"); + } + } + + app.mark_posted_until(now); + } + } + } +} + +fn no_updates() { + tracing::info!("got no updates for this period"); +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..70c0f22 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,229 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use eyre::Context; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub use upload_id::*; + +use crate::schemas::novel::{FullNovel, Novel}; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct State { + pub posted_until: Option, +} + +#[derive(Debug, Clone)] +pub struct Db { + novels: PathBuf, + + state_path: PathBuf, + state: State, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Entry { + pub info: PathBuf, + pub thumbnail: PathBuf, + + pub screenshots: PathBuf, + pub files: PathBuf, + pub upload_queue: PathBuf, +} + +impl Entry { + pub fn finish_upload(&self, id: UploadId, file_name: &str) -> eyre::Result<()> { + let src = self.upload_queue.join(id.to_file_name()); + let dest = match id.kind { + UploadKind::Thumbnail => self.thumbnail.clone(), + UploadKind::Screenshot => self.screenshots.join(file_name), + UploadKind::File => self.files.join(file_name), + }; + + fs::rename(src, dest).wrap_err("failed to mark upload as finished")?; + + Ok(()) + } + + pub fn start_upload(&self, slug: impl Into, kind: UploadKind) -> (UploadId, PathBuf) { + let id = UploadId { + id: Uuid::new_v4(), + kind, + novel: slug.into(), + }; + + let path = self.upload_queue.join(id.to_file_name()); + (id, path) + } +} + +impl Entry { + pub fn read_info(&self) -> eyre::Result { + let raw_info = fs::read_to_string(&self.info).wrap_err("failed to read info")?; + let info: Novel = toml::from_str(&raw_info).wrap_err("failed to deserialize TOML")?; + + Ok(info) + } + + pub fn get_full_info(&self) -> eyre::Result { + let info = self.read_info()?; + + Ok(FullNovel { + data: info, + upload_queue: self.upload_queue()?, + files: self.list_files()?, + screenshots: self.list_screenshots()?, + }) + } + + pub fn write_info(&self, info: &Novel) -> eyre::Result<()> { + let s = toml::to_string_pretty(info).wrap_err("failed to serialize novel")?; + fs::write(&self.info, &s)?; + + Ok(()) + } + + pub fn screenshot(&self, name: &str) -> eyre::Result { + let path = self.files.join(name); + eyre::ensure!(path.exists(), "the screenshot does not exist"); + + Ok(path) + } + + pub fn file(&self, name: &str) -> eyre::Result { + let path = self.files.join(name); + eyre::ensure!(path.exists(), "the file does not exist"); + + Ok(path) + } + + fn listdir(dir: &Path) -> eyre::Result> { + let mut out = Vec::new(); + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + let last = path.components().last().unwrap(); + let last = last.as_os_str().to_string_lossy().to_string(); + out.push(last); + } + + Ok(out) + } + + pub fn list_screenshots(&self) -> eyre::Result> { + Self::listdir(&self.screenshots) + } + + pub fn list_files(&self) -> eyre::Result> { + Self::listdir(&self.files) + } + + pub fn upload_queue(&self) -> eyre::Result> { + Self::listdir(&self.upload_queue) + } + + pub fn init(&self, info: &Novel) -> eyre::Result<()> { + if self.info.exists() || self.files.exists() { + eyre::bail!("this entry is already exists"); + } + + fs::create_dir_all(&self.screenshots).wrap_err("failed to create screenshots dir")?; + fs::create_dir_all(&self.files).wrap_err("failed to create files dir")?; + fs::create_dir_all(&self.upload_queue).wrap_err("failed to create upload queue")?; + self.write_info(info)?; + + Ok(()) + } + + pub fn exists(&self) -> bool { + self.info.exists() + } +} + +fn create_dir(p: impl AsRef) -> eyre::Result<()> { + let p = p.as_ref(); + if p.exists() { + return Ok(()); + } + + tracing::info!("creating directory {}", p.display()); + fs::create_dir_all(p)?; + Ok(()) +} + +impl Db { + pub fn state(&self) -> &State { + &self.state + } + pub fn update_state(&mut self, f: impl FnOnce(&mut State) -> O) -> O { + let out = f(&mut self.state); + let serialized = toml::to_string_pretty(&self.state).unwrap(); + + fs::write(&self.state_path, serialized).unwrap(); + out + } + + pub fn from_root(root: impl AsRef) -> eyre::Result { + let root = root.as_ref(); + fs::create_dir_all(root)?; + let novels = root.join("novels"); + let state_path = root.join("state.toml"); + + if !state_path.exists() { + fs::write( + &state_path, + toml::to_string_pretty(&State::default()).unwrap(), + ) + .wrap_err("failed to create state")?; + } + + create_dir(root)?; + create_dir(&novels)?; + + let state = toml::from_str(&fs::read_to_string(&state_path).unwrap()) + .wrap_err("failed to read state")?; + + Ok(Self { + novels, + state, + state_path, + }) + } +} + +impl Db { + pub fn enumerate(&self) -> Vec<(String, Entry)> { + let read = fs::read_dir(&self.novels).unwrap(); + let mut entries = vec![]; + + for entry in read.map(|e| e.unwrap().path()) { + let slug = entry.file_stem().unwrap().to_str().unwrap(); + entries.push((slug.to_owned(), self.entry(slug))); + } + + entries + } + + pub fn entry(&self, slug: &str) -> Entry { + let root = self.novels.join(slug); + let thumbnail = root.join("thumbnail"); + let screenshots = root.join("screenshots"); + let upload_queue = root.join("upload_queue"); + + let info = root.join("info.toml"); + let files = root.join("files"); + + Entry { + info, + files, + upload_queue, + thumbnail, + screenshots, + } + } +} + +mod upload_id; diff --git a/src/db/upload_id.rs b/src/db/upload_id.rs new file mode 100644 index 0000000..e679553 --- /dev/null +++ b/src/db/upload_id.rs @@ -0,0 +1,94 @@ +use std::{fmt, str::FromStr}; + +use eyre::OptionExt; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UploadKind { + Thumbnail, + Screenshot, + File, +} + +impl UploadKind { + pub const fn as_str(self) -> &'static str { + match self { + Self::Thumbnail => "t", + Self::Screenshot => "s", + Self::File => "f", + } + } +} + +impl FromStr for UploadKind { + type Err = eyre::Report; + + fn from_str(s: &str) -> Result { + Ok(match s { + "t" => Self::Thumbnail, + "s" => Self::Screenshot, + "f" => Self::File, + _ => eyre::bail!("wrong upload kind"), + }) + } +} + +#[derive(Debug)] +pub struct UploadId { + pub novel: String, + pub id: Uuid, + pub kind: UploadKind, +} + +impl UploadId { + pub fn to_file_name(&self) -> String { + format!("{}-{}", self.id, self.kind.as_str()) + } +} + +impl FromStr for UploadId { + type Err = eyre::Report; + + fn from_str(s: &str) -> Result { + let Some((novel, id_and_kind)) = s.rsplit_once('/') else { + eyre::bail!("invalid upload id") + }; + if id_and_kind.is_empty() { + eyre::bail!("invalid upload id"); + } + let (id, kind) = id_and_kind.split_at(id_and_kind.len().checked_sub(1).ok_or_eyre("loh")?); + + Ok(Self { + novel: novel.to_owned(), + kind: kind.parse()?, + id: id.parse()?, + }) + } +} + +impl fmt::Display for UploadId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}{}", self.novel, self.id, self.kind.as_str()) + } +} + +impl Serialize for UploadId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for UploadId { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = <&str>::deserialize(deserializer)?; + s.parse().map_err(|e| serde::de::Error::custom(e)) + } +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..deb7f27 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,30 @@ +use axum::{ + extract::Request, + middleware::Next, + response::{IntoResponse, Response}, +}; + +use crate::state::{Router, State}; + +fn deny() -> Response { + crate::result::Err::from(eyre::eyre!("nope")).into_response() +} + +async fn validate_secret(state: State, request: Request, next: Next) -> Response { + let Some(val) = request.headers().get("x-secret") else { + return deny(); + }; + + if val != state.secret.as_str() { + return deny(); + } + next.run(request).await +} + +pub fn make(state: State) -> Router { + Router::new() + .nest("/novels", novels::make()) + .layer(axum::middleware::from_fn_with_state(state, validate_secret)) +} + +mod novels; diff --git a/src/http/novels.rs b/src/http/novels.rs new file mode 100644 index 0000000..3721d38 --- /dev/null +++ b/src/http/novels.rs @@ -0,0 +1,12 @@ +use crate::state::Router; + +pub fn make() -> Router { + Router::new() + .nest("/:slug", specific::make()) + .merge(list::make()) + .nest("/pull_next", pull_next::make()) +} + +mod list; +mod pull_next; +mod specific; diff --git a/src/http/novels/list.rs b/src/http/novels/list.rs new file mode 100644 index 0000000..0fb3de2 --- /dev/null +++ b/src/http/novels/list.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; + +use axum::routing::post; +use serde::Serialize; + +use crate::{ + result::{ApiResult, FromJson}, + schemas::novel::FullNovel, + state::{Router, State}, +}; + +#[derive(Debug, serde::Deserialize)] +struct Args { + pub start_from: Option, +} + +#[derive(Debug, Serialize, Default)] +struct Output { + pub novels: HashMap, + pub errors: Vec, +} + +async fn list(state: State, FromJson(args): FromJson) -> ApiResult { + let mut out = Output::default(); + let novels = state.app.read().enumerate_novels(); + + for (slug, entry) in novels { + let novel = match entry.get_full_info() { + Ok(n) => n, + Err(e) => { + out.errors.push(e.to_string()); + continue; + } + }; + + if let Some(ref start_from) = args.start_from { + if &novel.data.post_at < start_from { + continue; + } + } + + out.novels.insert(slug, novel); + } + + Ok(out.into()) +} + +pub fn make() -> Router { + Router::new().route("/", post(list)) +} diff --git a/src/http/novels/pull_next.rs b/src/http/novels/pull_next.rs new file mode 100644 index 0000000..f2dd4c4 --- /dev/null +++ b/src/http/novels/pull_next.rs @@ -0,0 +1,24 @@ +use std::time::Duration; + +use crate::{ + result::ApiResult, + state::{Router, State}, +}; + +use axum::routing::get; +use tokio::time; + +async fn pull_next(state: State) -> ApiResult> { + let rx = state.app.write().subs.subscribe(); + + let Ok(Ok(r)) = time::timeout(Duration::from_secs(12 * 60 * 60), rx).await else { + tracing::debug!("timed out for subscription"); + return Ok(None.into()); + }; + + Ok(Some(r).into()) +} + +pub fn make() -> Router { + Router::new().route("/", get(pull_next)) +} diff --git a/src/http/novels/specific/enqueue.rs b/src/http/novels/specific/enqueue.rs new file mode 100644 index 0000000..0803117 --- /dev/null +++ b/src/http/novels/specific/enqueue.rs @@ -0,0 +1,43 @@ +use axum::{ + extract::Path, + routing::{patch, post}, +}; + +use crate::{ + result::{ApiResult, FromJson}, + schemas::{novel::Novel, sanity::Slug}, + state::{Router, State}, +}; + +async fn update( + Path(slug): Path, + state: State, + FromJson(novel): FromJson, +) -> ApiResult { + let mut app = state.app.write(); + let entry = app.novel(&slug.0)?; + entry.write_info(&novel)?; + + // hack, I just don't care, updates are + // infrequent. + app.reload_queue()?; + + Ok(true.into()) +} + +async fn enqueue( + Path(slug): Path, + state: State, + FromJson(novel): FromJson, +) -> ApiResult { + let mut app = state.app.write(); + app.add_novel(slug.0, novel)?; + + Ok(true.into()) +} + +pub fn make() -> Router { + Router::new() + .route("/", post(enqueue)) + .route("/", patch(update)) +} diff --git a/src/http/novels/specific/get.rs b/src/http/novels/specific/get.rs new file mode 100644 index 0000000..0410fc2 --- /dev/null +++ b/src/http/novels/specific/get.rs @@ -0,0 +1,18 @@ +use axum::{extract::Path, routing::get}; + +use crate::{ + result::ApiResult, + schemas::{novel::FullNovel, sanity::Slug}, + state::{Router, State}, +}; + +async fn get_novel(Path(slug): Path, state: State) -> ApiResult { + let entry = state.app.read().novel(&slug.0)?; + let novel = entry.get_full_info()?; + + Ok(novel.into()) +} + +pub fn make() -> Router { + Router::new().route("/", get(get_novel)) +} diff --git a/src/http/novels/specific/mod.rs b/src/http/novels/specific/mod.rs new file mode 100644 index 0000000..7e4e7f7 --- /dev/null +++ b/src/http/novels/specific/mod.rs @@ -0,0 +1,13 @@ +use crate::state::Router; + +pub fn make() -> Router { + // TODO: Validation here. + Router::new() + .merge(enqueue::make()) + .merge(get::make()) + .nest("/upload", upload::make()) +} + +mod enqueue; +mod get; +mod upload; diff --git a/src/http/novels/specific/upload.rs b/src/http/novels/specific/upload.rs new file mode 100644 index 0000000..9409813 --- /dev/null +++ b/src/http/novels/specific/upload.rs @@ -0,0 +1,120 @@ +use std::{fs, io::Write}; + +use axum::{ + extract::{Multipart, Path}, + routing::{delete, post}, +}; +use eyre::Context; + +use crate::{ + db::{UploadId, UploadKind}, + result::{ApiResult, FromJson}, + schemas::sanity::{FileName, Slug}, + state::{Router, State}, +}; + +async fn make_upload( + slug: String, + kind: UploadKind, + state: State, + mut multipart: Multipart, +) -> ApiResult { + let (id, dest) = { + let app = state.app.read(); + app.begin_upload(&slug, kind)? + }; + + let Some(mut field) = multipart + .next_field() + .await + .wrap_err("failed to get field")? + else { + return Err(eyre::eyre!("empty multipart").into()); + }; + + let mut file = fs::File::options() + .read(true) + .write(true) + .create(true) + .open(dest) + .wrap_err("failed to open dest file")?; + while let Some(chunk) = field.chunk().await.wrap_err("failed to read chunk")? { + file.write(&chunk).wrap_err("failed to write chunk")?; + } + + Ok(id.into()) +} + +async fn up_screenshot( + Path(slug): Path, + state: State, + multipart: Multipart, +) -> ApiResult { + make_upload(slug, UploadKind::Screenshot, state, multipart).await +} + +async fn up_file( + Path(slug): Path, + state: State, + multipart: Multipart, +) -> ApiResult { + make_upload(slug, UploadKind::File, state, multipart).await +} + +async fn up_thumbnail( + Path(slug): Path, + state: State, + multipart: Multipart, +) -> ApiResult { + make_upload(slug, UploadKind::Thumbnail, state, multipart).await +} + +#[derive(Debug, serde::Deserialize)] +struct Finish { + pub id: UploadId, + pub file_name: FileName, +} + +async fn finish( + Path(slug): Path, + state: State, + FromJson(args): FromJson, +) -> ApiResult { + let app = state.app.read(); + app.finish_upload(&slug.0, args.id, &args.file_name.0)?; + Ok(true.into()) +} + +#[derive(Debug, serde::Deserialize)] +struct DeleteFile { + pub name: FileName, + pub kind: UploadKind, +} + +async fn delete_file( + Path(slug): Path, + state: State, + FromJson(args): FromJson, +) -> ApiResult { + let app = state.app.read(); + let novel = app.novel(&slug)?; + + let path = match args.kind { + UploadKind::Thumbnail => novel.thumbnail.clone(), + UploadKind::Screenshot => novel.screenshot(&args.name.0)?, + UploadKind::File => novel.file(&args.name.0)?, + }; + + fs::remove_file(path).wrap_err("failed to delete")?; + + Ok(true.into()) +} + +pub fn make() -> Router { + Router::new() + .route("/screenshot", post(up_screenshot)) + .route("/file", post(up_file)) + .route("/thumbnail", post(up_thumbnail)) + .route("/finish", post(finish)) + .route("/", delete(delete_file)) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e5d69a1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,17 @@ +pub mod http; + +pub mod db; +pub mod schemas; + +pub mod app; +pub mod post_queue; + +pub mod result; + +pub mod daemon; +pub mod notifies; +pub mod subs; + +pub mod state; + +pub mod cfg; diff --git a/src/notifies.rs b/src/notifies.rs new file mode 100644 index 0000000..0657e42 --- /dev/null +++ b/src/notifies.rs @@ -0,0 +1,16 @@ +use std::sync::Arc; + +use tokio::sync::Notify; + +#[derive(Debug)] +pub struct Notifies { + pub update: Notify, + pub new_subscriber: Notify, +} + +pub fn make() -> Arc { + Arc::new(Notifies { + update: Notify::new(), + new_subscriber: Notify::new(), + }) +} diff --git a/src/post_queue.rs b/src/post_queue.rs new file mode 100644 index 0000000..04356ca --- /dev/null +++ b/src/post_queue.rs @@ -0,0 +1,105 @@ +use std::{ + cmp::{Ordering, Reverse}, + ops::Deref, +}; + +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct Item { + pub novel: String, + pub post_at: jiff::Zoned, +} + +pub struct Closest<'a> { + queue: &'a mut Queue, +} + +impl<'a> Closest<'a> { + pub fn try_pop(self, now: &jiff::Zoned) -> Option { + let last = self.queue.entries.pop().unwrap(); + + if now >= &last.post_at { + Some(last) + } else { + self.queue.entries.push(last); + None + } + } +} + +impl<'a> Deref for Closest<'a> { + type Target = Item; + + fn deref(&self) -> &Self::Target { + let len = self.queue.entries.len() - 1; + &self.queue.entries[len] + } +} + +fn cmp_items(lhs: &Item, rhs: &Item) -> Ordering { + Reverse(&lhs.post_at).cmp(&Reverse(&rhs.post_at)) +} + +#[derive(Debug, Default)] +pub struct Queue { + entries: Vec, +} + +impl FromIterator for Queue { + fn from_iter>(iter: T) -> Self { + let mut entries: Vec = iter.into_iter().collect(); + entries.sort_unstable_by(cmp_items); + + #[cfg(debug_assertions)] + if entries.len() >= 2 { + let first = &entries[0].post_at; + let second = &entries[1].post_at; + + assert!(first >= second); + } + + Self { entries } + } +} + +impl Queue { + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn closest(&self) -> Option<&Item> { + self.entries.last() + } + + pub fn closest_mut(&mut self) -> Option> { + if self.entries.is_empty() { + None + } else { + Some(Closest { queue: self }) + } + } + + pub fn add(&mut self, item: Item) { + // This could be: + // let Ok(idx) | Err(idx) = ..; + // But fuck rust, it's not allowed. + let res = self + .entries + .binary_search_by(|probe| cmp_items(probe, &item)); + + // The why блядь? + let idx = match res { + Ok(i) => i, + Err(i) => i, + }; + + self.entries.insert(idx, item); + } + + pub const fn new() -> Self { + Self { + entries: Vec::new(), + } + } +} diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 0000000..f2e7d18 --- /dev/null +++ b/src/result.rs @@ -0,0 +1,65 @@ +use axum::{body::Body, extract::FromRequest, response::IntoResponse}; +use serde::{Deserialize, Serialize}; + +pub type ApiResult = Result, Err>; + +#[derive(Debug, Clone)] +pub struct FromJson(pub T); + +#[axum::async_trait] +impl Deserialize<'de>, S: Send + Sync> FromRequest for FromJson { + type Rejection = Err; + + async fn from_request( + request: axum::http::Request, + s: &S, + ) -> Result { + let js = axum::Json::from_request(request, s) + .await + .map_err(|rej| eyre::eyre!("failed to parse json: {}", rej.to_string())) + .inspect_err(|e| tracing::error!(error = %e, "got error"))?; + Ok(Self(js.0)) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Ok { + pub ok: T, +} + +impl From for Ok { + fn from(value: T) -> Self { + Self { ok: value } + } +} + +impl IntoResponse for Ok { + fn into_response(self) -> axum::response::Response { + axum::Json(self).into_response() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Err { + pub error: ApiError, +} + +impl From for Err { + fn from(value: eyre::Report) -> Self { + Self { + error: ApiError::Custom(format!("{value:?}")), + } + } +} + +impl IntoResponse for Err { + fn into_response(self) -> axum::response::Response { + axum::Json(self).into_response() + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ApiError { + Custom(String), +} diff --git a/src/schemas/mod.rs b/src/schemas/mod.rs new file mode 100644 index 0000000..e38b9d6 --- /dev/null +++ b/src/schemas/mod.rs @@ -0,0 +1,4 @@ +pub mod novel; +pub mod platform; + +pub mod sanity; diff --git a/src/schemas/novel.rs b/src/schemas/novel.rs new file mode 100644 index 0000000..5f8e38c --- /dev/null +++ b/src/schemas/novel.rs @@ -0,0 +1,44 @@ +use std::num::NonZeroU8; + +use serde::{Deserialize, Serialize}; + +use super::platform::Platform; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Download { + pub file_name: String, + pub platform: Platform, +} + +#[derive(Debug, Clone, Serialize)] +pub struct StoredNovel { + #[serde(flatten)] + pub novel: Novel, + + pub modified_at: jiff::Zoned, + pub created_at: jiff::Zoned, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FullNovel { + pub data: Novel, + + pub upload_queue: Vec, + pub files: Vec, + pub screenshots: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Novel { + pub title: String, + pub description: String, + + pub vndb: Option, + pub hours_to_read: Option, + + pub tags: Vec, + pub genres: Vec, + + pub tg_post: Option, + pub post_at: jiff::Zoned, +} diff --git a/src/schemas/platform.rs b/src/schemas/platform.rs new file mode 100644 index 0000000..6fa79d6 --- /dev/null +++ b/src/schemas/platform.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged, rename_all = "snake_case")] +pub enum Platform { + Android, + Linux, + Windows, + Ios, + Other(String), +} diff --git a/src/schemas/sanity.rs b/src/schemas/sanity.rs new file mode 100644 index 0000000..6233a43 --- /dev/null +++ b/src/schemas/sanity.rs @@ -0,0 +1,145 @@ +use serde::{Deserialize, Serialize}; + +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; + +// Copypasta from . +fn normalize(p: &Path) -> PathBuf { + let mut stack: Vec = vec![]; + + // We assume .components() removes redundant consecutive path separators. + // Note that .components() also does some normalization of '.' on its own anyways. + // This '.' normalization happens to be compatible with the approach below. + for component in p.components() { + match component { + // Drop CurDir components, do not even push onto the stack. + Component::CurDir => {} + + // For ParentDir components, we need to use the contents of the stack. + Component::ParentDir => { + // Look at the top element of stack, if any. + let top = stack.last().cloned(); + + match top { + // A component is on the stack, need more pattern matching. + Some(c) => { + match c { + // Push the ParentDir on the stack. + Component::Prefix(_) => { + stack.push(component); + } + + // The parent of a RootDir is itself, so drop the ParentDir (no-op). + Component::RootDir => {} + + // A CurDir should never be found on the stack, since they are dropped when seen. + Component::CurDir => { + unreachable!(); + } + + // If a ParentDir is found, it must be due to it piling up at the start of a path. + // Push the new ParentDir onto the stack. + Component::ParentDir => { + stack.push(component); + } + + // If a Normal is found, pop it off. + Component::Normal(_) => { + let _ = stack.pop(); + } + } + } + + // Stack is empty, so path is empty, just push. + None => { + stack.push(component); + } + } + } + + // All others, simply push onto the stack. + _ => { + stack.push(component); + } + } + } + + // If an empty PathBuf would be return, instead return CurDir ('.'). + if stack.is_empty() { + return PathBuf::from(Component::CurDir.as_os_str()); + } + + let mut norm_path = PathBuf::new(); + + for item in &stack { + norm_path.push(item.as_os_str()); + } + + norm_path +} + +#[derive(Debug, Clone)] +pub struct Slug(pub String); + +impl Serialize for Slug { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Slug { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + ensure_slug(&s).map_err(|e| serde::de::Error::custom(e))?; + Ok(Self(s)) + } +} + +pub fn ensure_slug(text: &str) -> eyre::Result<()> { + fn is_slug(s: &str) -> bool { + !s.is_empty() + && s.chars() + .all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || matches!(c, '-' | '_')) + } + + eyre::ensure!(is_slug(text), "not a slug"); + Ok(()) +} + +#[derive(Debug, Clone)] +pub struct FileName(pub String); + +impl<'de> Deserialize<'de> for FileName { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let s = PathBuf::from(s); + + let res = normalize(&s); + let res = res + .file_name() + .ok_or_else(|| serde::de::Error::custom("the fuck"))? + .to_str() + .ok_or_else(|| serde::de::Error::custom("nigger"))?; + + Ok(Self(res.to_owned())) + } +} + +impl Serialize for FileName { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..b87b2ff --- /dev/null +++ b/src/state.rs @@ -0,0 +1,37 @@ +use std::{ops::Deref, sync::Arc}; + +use crate::{app::App, cfg}; + +use axum::extract::FromRequestParts; +use parking_lot::RwLock; + +pub type Router = axum::Router; + +#[derive(Debug)] +pub struct LockedState { + pub app: RwLock, + pub cfg: cfg::Root, + pub secret: String, +} + +#[derive(Debug, Clone, FromRequestParts)] +#[from_request(via(axum::extract::State))] +pub struct State(Arc); + +impl State { + pub fn new(app: App, cfg: cfg::Root) -> eyre::Result { + Ok(Self(Arc::new(LockedState { + app: RwLock::new(app), + secret: cfg.app.secret.into_string()?, + cfg, + }))) + } +} + +impl Deref for State { + type Target = LockedState; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/src/subs.rs b/src/subs.rs new file mode 100644 index 0000000..d4dc9c4 --- /dev/null +++ b/src/subs.rs @@ -0,0 +1,36 @@ +use tokio::sync::oneshot; + +use std::sync::Arc; + +use crate::notifies::Notifies; + +#[derive(Debug)] +pub struct Subs { + queue: Vec>, + notifies: Arc, +} + +impl Subs { + pub const fn new(notifies: Arc) -> Self { + Self { + queue: Vec::new(), + notifies, + } + } + + pub fn has_listeners(&self) -> bool { + self.queue.len() != 0 + } + + pub fn consume(&mut self) -> Vec> { + std::mem::take(&mut self.queue) + } + + pub fn subscribe(&mut self) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + self.queue.push(tx); + self.notifies.new_subscriber.notify_waiters(); + + rx + } +}