diff --git a/.gitignore b/.gitignore index e631a67..46a48a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -upload data.db -target/* -logs/* +pack.yml +upload/ +target/ +logs/ diff --git a/Cargo.lock b/Cargo.lock index 3fce1ba..c748e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,48 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.100", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -120,6 +162,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -871,6 +922,7 @@ name = "pack" version = "0.0.1" dependencies = [ "anyhow", + "askama", "axum", "bytes", "env_filter", @@ -1093,6 +1145,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.0.5" @@ -2099,6 +2157,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 66bec94..cc7b8a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ authors = ["Kenneth Jao"] [dependencies] anyhow = "1.0.98" +askama = { version = "0.14.0", features = ["blocks"] } axum = { version = "0.8.1", features = ["multipart"] } bytes = "1.10.1" env_filter = "0.1.3" diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..d122a5b Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/fragile.svg b/assets/fragile.svg new file mode 100644 index 0000000..ed630f3 --- /dev/null +++ b/assets/fragile.svg @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/assets/keep_dry.svg b/assets/keep_dry.svg new file mode 100644 index 0000000..fa22250 --- /dev/null +++ b/assets/keep_dry.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/qr_code.svg b/assets/qr_code.svg new file mode 100644 index 0000000..6bf316b --- /dev/null +++ b/assets/qr_code.svg @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/recycle.svg b/assets/recycle.svg new file mode 100644 index 0000000..70be3c8 --- /dev/null +++ b/assets/recycle.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/pack.yml b/pack.yml index 565d0ea..a676345 100644 --- a/pack.yml +++ b/pack.yml @@ -1,6 +1,7 @@ --- -host: "192.168.0.220" +host: "127.0.0.1" port: "3000" +domain: "http://127.0.0.1:3000/" db_path: "data.db" log_path: "logs" upload_path: "upload" diff --git a/pack.yml.template b/pack.yml.template new file mode 100644 index 0000000..5aade98 --- /dev/null +++ b/pack.yml.template @@ -0,0 +1,10 @@ +--- +host: "127.0.0.1" +port: "3000" +domain: +db_path: "data.db" +log_path: "logs" +upload_path: "upload" +gitea_host: +client_id: +client_secret: diff --git a/src/api.rs b/src/api.rs index ef854fb..0c68e89 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,4 +1,4 @@ -use std::{fs, path, slice::Iter}; +use std::{fs, fmt, path, slice::Iter}; use bytes::Bytes; use std::process::Command; use rand::distr::{Alphanumeric, SampleString}; @@ -17,9 +17,9 @@ use serde_json::Value; use crate::{query, ServerState}; -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy)] #[repr(u8)] -enum RepoFeature { +pub enum RepoFeature { Docs = 0, Builds = 1, Nightly = 2, @@ -35,6 +35,32 @@ impl RepoFeature { } } +impl fmt::Display for RepoFeature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +impl From for String { + fn from(value: RepoFeature) -> Self { + value.to_string().to_lowercase() + } +} + +impl TryFrom<&str> for RepoFeature { + type Error = APIError; + + fn try_from(value: &str) -> Result { + for feature in RepoFeature::iter() { + if feature.to_string().to_lowercase() == value.to_lowercase() { + return Ok(*feature); + } + } + Err(APIError::InvalidFeature { feature: value.to_owned() }) + } +} + + #[derive(Error, Debug)] pub enum APIError { #[error("Request error: {0}")] @@ -105,37 +131,12 @@ impl IntoResponse for APIError { } } -impl From for &str { - fn from(value: RepoFeature) -> Self { - match value { - RepoFeature::Docs => "docs", - RepoFeature::Builds => "builds", - RepoFeature::Nightly => "nightly", - RepoFeature::PreProd => "preprod", - } - } -} - -impl TryFrom<&str> for RepoFeature { - type Error = APIError; - - fn try_from(value: &str) -> Result { - match value.to_lowercase().as_str() { - "docs" => Ok(RepoFeature::Docs), - "builds" => Ok(RepoFeature::Builds), - "nightly" => Ok(RepoFeature::Nightly), - "preprod" => Ok(RepoFeature::PreProd), - other => Err(APIError::InvalidFeature { feature: other.to_owned() }), - } - } -} - fn as_feature_list(value: u64) -> FeatureList { let mut v: Vec = Vec::new(); for feature in RepoFeature::iter() { let feat_num = 1 << (*feature as u64); if (value & feat_num) == feat_num { - v.push(Into::<&str>::into(*feature).to_owned()); + v.push(Into::::into(*feature).to_owned()); } } v diff --git a/src/compile.rs b/src/compile.rs new file mode 100644 index 0000000..94eb1c3 --- /dev/null +++ b/src/compile.rs @@ -0,0 +1,104 @@ +use anyhow::{Context, Result}; +use askama::Template; +use std::{fs, io, path::Path}; + +#[derive(Clone, Copy)] +struct WebStates<'a> { + auth: &'a str, + error: &'a str, + loading: &'a str, + repo_selected: &'a str, + repo_created: &'a str, + button_on: &'a str, + feature_on: &'a str, + confirm_on: &'a str, +} + +#[derive(Template)] +#[template( + ext = "html", + path = "html/contentDefault.html", + whitespace = "suppress" +)] +struct Default<'a> { + config: crate::ServerConfig, + features: Vec, + content_text: String, + state: WebStates<'a>, +} + +#[derive(Template)] +#[template(ext = "html", path = "html/base.html", whitespace = "suppress")] +struct NotFound { + config: crate::ServerConfig, + content_text: String, +} + +#[derive(Template)] +#[template(path = "index.css", escape = "txt", whitespace = "suppress")] +struct Css<'a> { + state: WebStates<'a>, +} + +pub fn make_templates(config: &crate::ServerConfig) -> Result<()> { + let states = WebStates { + auth: "auth", + error: "error", + loading: "loading", + repo_selected: "repoSelected", + repo_created: "repoCreated", + button_on: "enabled", + feature_on: "fEnabled", + confirm_on: "enabled", + }; + + let default = Default { + config: config.clone(), + features: crate::api::RepoFeature::iter() + .map(|f| f.to_string()) + .collect(), + content_text: "awaiting authorization...".to_owned(), + state: states, + } + .render() + .context("Unable to process 'Default' template")?; + + fs::write("static/index.html", default).context("Could not write to static/index.html")?; + + let not_found = NotFound { + config: config.clone(), + content_text: "404: not found...".to_owned(), + } + .render() + .context("Unable to process 'NotFound' template.")?; + + fs::write("static/404.html", not_found).context("Could not write to static/404.html")?; + + let css = Css { state: states } + .render() + .context("Unable to process 'CSS' template.")?; + + fs::write("static/index.css", css).context("Could not write to static/index.css")?; + + Ok(()) +} + +fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + fs::create_dir_all(&dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} + +pub fn copy_assets() -> Result<()> { + fs::create_dir("static").context("Could not create directory 'static'.")?; + copy_dir_all("assets", "static").context("Could not copy 'assets' into 'static'.")?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index a33df9f..c5ad183 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ use tracing_appender::rolling; use serde::Deserialize; mod api; +mod compile; mod util; type SQLConn = sqlite::ConnectionThreadSafe; @@ -34,6 +35,7 @@ type SQLConn = sqlite::ConnectionThreadSafe; struct ServerConfig { host: String, port: String, + domain: String, db_path: String, log_path: String, upload_path: String, @@ -81,9 +83,12 @@ async fn main() -> Result<()> { .context(format!("Failed to setup SQLite database at '{}'.", &config.db_path))?; // Make file upload folder. - fs::create_dir_all(&config.upload_path)?; + fs::create_dir_all(&config.upload_path) + .context(format!("Unable to make directory at '{}'.", &config.upload_path))?; let log_file = rolling::daily(&config.log_path, ""); let upload_path = config.upload_path.clone(); + compile::copy_assets()?; + compile::make_templates(&config)?; let sql: &'static sqlite::ConnectionThreadSafe = Box::leak(Box::new(sql)); let server_state = ServerState { config, sql }; @@ -118,10 +123,8 @@ async fn main() -> Result<()> { method = format!("{}", request.method()), version = format!("{:?}", request.version()), remote_ip = addr, - ) }); - // Build routes let api_routes = Router::new() diff --git a/static/404.html b/static/404.html new file mode 100644 index 0000000..e4410aa --- /dev/null +++ b/static/404.html @@ -0,0 +1,272 @@ + + + + + Pack + + + + + + + + + + + + + + + + + + + + + + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BYTE TRANSIT FEES PAID + 64 OF 64 + 6.28 TB FIBRE OPTIC EXPRESS RATE + ACCEPTS TAR.GZ, ZIP + + + + + + + + + + + + + 404: NOT FOUND... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +http://127.0.0.1:3000/ + + + + +
BYTE TRANSIT FEES PAID
64 OF 64
6.28 TB FIBRE OPTIC EXPRESS RATE
ACCEPTS TAR.GZ, ZIP