diff options
| author | Nikolay Govorov <me@govorov.online> | 2026-01-15 22:51:56 +0000 |
|---|---|---|
| committer | Nikolay Govorov <me@govorov.online> | 2026-01-15 22:51:56 +0000 |
| commit | fc45ea8867743f323bb2ede8c0ef14ef13272b87 (patch) | |
| tree | 514929d8c178d57228d6a77153417ea5c3ef93fb | |
| parent | 8001852bf353f38d2428f4a28aab32570ae73754 (diff) | |
| download | tar tar.gz tar.bz2 tar.lz tar.xz tar.zst zip | |
Update project structure
Diffstat
| -rw-r--r-- | Cargo.lock | 22 | +22 −0 |
| -rw-r--r-- | Cargo.toml | 3 | +2 −1 |
| -rw-r--r-- | src/backends/mod.rs | 4 | +4 −0 |
| -rw-r--r-- | src/backends/zig.rs (renamed from src/controller_zig.rs) | 34 | +20 −14 |
| -rw-r--r-- | src/config.rs (renamed from src/service_config.rs) | 0 | +0 −0 |
| -rw-r--r-- | src/controller_web.rs | 36 | +0 −36 |
| -rw-r--r-- | src/index.html | 100 | +0 −100 |
| -rw-r--r-- | src/main.rs | 270 | +233 −37 |
| -rw-r--r-- | src/storage.rs (renamed from src/service_storage.rs) | 2 | +1 −1 |
| -rw-r--r-- | src/upstream.rs (renamed from src/service_upstream.rs) | 0 | +0 −0 |
10 files changed, 282 insertions, 189 deletions
diff --git a/Cargo.lock b/Cargo.lock index bd1d6bc..e9ea97e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,6 +200,27 @@ dependencies = [ ] [[package]] +name = "axum-extra" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] name = "axum-macros" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4944,6 +4965,7 @@ name = "zorian" version = "0.1.0" dependencies = [ "axum", + "axum-extra", "bytes", "cargo-deny", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 11e5f9a..85f596c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/mrdimidium/Zorian" [dependencies] axum = { version = "0.8.8", features = ["http2", "macros", "multipart"] } +axum-extra = "0.12.5" bytes = "1.11.0" chrono = "0.4" crc32fast = "1.4.2" @@ -25,10 +26,10 @@ sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "macros", "de thiserror = "2.0" tokio = { version = "1.49.0", features = ["full"] } toml = "0.9.8" +tower = "0.5.3" tower-http = { version = "0.6.8", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } -tower = "0.5.3" uuid = { version = "1.19.0", features = ["v4","v5"] } [dev-dependencies] diff --git a/src/backends/mod.rs b/src/backends/mod.rs new file mode 100644 --- /dev/null +++ b/src/backends/mod.rs @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online> +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod zig; diff --git a/src/controller_zig.rs b/src/backends/zig.rs index 7e9e6a8..44ac3bc 100644 --- a/src/controller_zig.rs +++ b/src/backends/zig.rs @@ -8,9 +8,9 @@ use semver::Version; use thiserror::Error; use tracing::error; -use crate::service_config; -use crate::service_storage; -use crate::service_upstream; +use crate::config; +use crate::storage; +use crate::upstream; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Archive { @@ -51,16 +51,22 @@ impl<'a> Tarball<'a> { let archive; let tarball_type; - // (?:|-bootstrap|-[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(\d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?)\.(?:tar\.xz|zip)(?:\.minisig)? + // (?:|-bootstrap|-[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-( + // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)? + // )\.(?:tar\.xz|zip)(?:\.minisig)? buffer = buffer.strip_prefix("zig-").ok_or(ParseError)?; - // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(\d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?)\.(?:tar\.xz|zip) + // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-( + // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)? + // )\.(?:tar\.xz|zip) if let Some(it) = buffer.strip_suffix(".minisig") { buffer = it; minisig = true; } - // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(\d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?) + // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-( + // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)? + // ) if let Some(it) = buffer.strip_suffix(".zip") { buffer = it; archive = Archive::Zip; @@ -142,16 +148,16 @@ impl<'a> Tarball<'a> { } pub struct ZigController { - config: Arc<service_config::ConfigService>, - storage: Arc<service_storage::StorageService>, - upstream: Arc<service_upstream::UpstreamService>, + config: Arc<config::ConfigService>, + storage: Arc<storage::StorageService>, + upstream: Arc<upstream::UpstreamService>, } impl ZigController { pub fn new( - config: Arc<service_config::ConfigService>, - storage: Arc<service_storage::StorageService>, - upstream: Arc<service_upstream::UpstreamService>, + config: Arc<config::ConfigService>, + storage: Arc<storage::StorageService>, + upstream: Arc<upstream::UpstreamService>, ) -> Self { Self { config, @@ -189,13 +195,13 @@ impl ZigController { let entry = controller .upstream - .fetch(service_upstream::DownloadRequest { url }) + .fetch(upstream::DownloadRequest { url }) .await?; match controller.storage.put("zig", &filename, &entry.bytes).await { Ok(()) => {} Err(_) => { - return Err(http::StatusCode::OK); + return Err(http::StatusCode::INTERNAL_SERVER_ERROR); } } diff --git a/src/service_config.rs b/src/config.rs index dd55bfc..dd55bfc 100644 --- a/src/service_config.rs +++ b/src/config.rs diff --git a/src/controller_web.rs b/src/controller_web.rs deleted file mode 100644 --- a/src/controller_web.rs +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online> -// SPDX-License-Identifier: AGPL-3.0-or-later - -use axum::{Router, extract, http, response, routing}; -use std::sync::Arc; - -/// Handles html pages rendering and static files -pub struct WebController { - jinja: minijinja::Environment<'static>, -} - -impl WebController { - pub fn new() -> Self { - let mut jinja = minijinja::Environment::new(); - jinja - .add_template("index", include_str!("./index.html")) - .unwrap(); - - Self { jinja } - } - - pub fn router(self: Arc<Self>) -> Router { - Router::new() - .route("/", routing::get(Self::index)) - .with_state(self) - } - - async fn index( - extract::State(controller): extract::State<Arc<Self>>, - ) -> Result<response::Html<String>, http::StatusCode> { - let template = controller.jinja.get_template("index").unwrap(); - let rendered = template.render(minijinja::context! {}).unwrap(); - - Ok(response::Html(rendered)) - } -} diff --git a/src/index.html b/src/index.html deleted file mode 100644 --- a/src/index.html +++ /dev/null @@ -1,100 +0,0 @@ -<!-- -SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online> -SPDX-License-Identifier: AGPL-3.0-or-later ---> -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width,initial-scale=1"> - <meta http-equiv="X-UA-Compatible" content="ie=edge"> - - <title>Earth PKG — tiny & opinionated packages mirror</title> - <style> - :root { - font-size: 1.125rem; - line-height: 1.4; - font-family: - 'Alegreya Sans', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, - 'Open Sans', 'Helvetica Neue', sans-serif; - } - - html { - margin: 0; - padding: 0; - } - - body { - margin: 0 auto; - padding: 1.5em 2em; - max-width: 680px; - } - - code { - font-size: .75rem; - } - - h1, h2, h3, h4, h5, h6, h7, h8, h9 { - font-weight: 700; - line-height: 1.2; - margin: 0; - } - - h1 { - font-size: 2.75rem; - } - - h1:first-child { - margin-top: 0; - } - - th { - text-align: start; - } - </style> - </head> - - <body> - <h1> - Earth PKG — tiny & opinionated packages mirror. - </h1> - - <content> - <p>This site provides a proxy for downloading zig installation files and dependencies. - On the one hand, this reduces the load on the original project's site - and makes your infrastructure more reliable by adding redundancy.</p> - - <p>Read more about community mirrors in the <a href="https://ziglang.org/download/community-mirrors/">blog post</a>. - Information on how to deploy your own mirror is available - <a href="https://github.com/ziglang/www.ziglang.org/blob/main/MIRRORS.md">in the documentation</a>.</p> - - <h2>Direct usage:</h2> - - <p>For simplicity, you can use tools like <a href="https://github.com/prantlf/zigup">prantlf/zigup</a> and - <a href="https://github.com/mlugg/setup-zig">mlugg/setup-zig</a>.</p> - - <p> - To install manually: - <ol> - <li>download zig dist file:<br><code>wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz</code>;</li> - <li>download zig minisign file:<br><code>wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig</code>;</li> - <li>check archive integrity:<br><code>minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U</code>;</li> - <li>unpack arhive:<br><code>tar -xf "zig-x86_64-linux-0.15.1.tar.xz"</code>;</li> - <li>check installed zig:<br><code>./zig-x86_64-linux-0.15.1/zig --version</code>.</li> - </ol> - - <city>You can take actual minisign public key in <a href="https://ziglang.org/download/">download page</a></city>. - </p> - - <h2>Privacy policy</h2> - - <p>This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it.</p> - - <p>Since the mirror is hosted on hardware, we collect access logs to combat bots and brute-force attacks. - The logs are used for security purposes and load planning, are not shared with third parties, - and are deleted after 30 days.</p> - - <p>Third-party analytics systems are not used same as client-side trackers.</p> - </content> -</html> diff --git a/src/main.rs b/src/main.rs index 6d74b09..d2317c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,25 @@ // SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online> // SPDX-License-Identifier: AGPL-3.0-or-later +mod backends; +mod config; +mod storage; +mod upstream; + +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; +use std::task::{Context, Poll}; -use axum::{Router, http}; +use axum::{ + Router, + body::Body, + extract, + http::{self, Request, Response}, + response::{self, IntoResponse}, + routing, +}; use tower_http::{ request_id, trace::{DefaultOnFailure, TraceLayer}, @@ -12,11 +27,7 @@ use tower_http::{ use tracing::{error, info}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -mod controller_web; -mod controller_zig; -mod service_config; -mod service_storage; -mod service_upstream; +use crate::backends::zig::ZigController; static REQUEST_ID_HEADER: http::HeaderName = http::HeaderName::from_static("x-request-id"); @@ -66,14 +77,14 @@ async fn main() { Some(path) => { info!("use config file from {}", path.to_str().unwrap()); - service_config::ConfigService::from_file(&path).unwrap_or_else(|e| { + config::ConfigService::from_file(&path).unwrap_or_else(|e| { error!("{e}"); std::process::exit(1); }) } None => { info!("configuration file path not provided"); - service_config::ConfigService::default() + config::ConfigService::default() } }); config.validate().unwrap_or_else(|e| { @@ -81,21 +92,17 @@ async fn main() { std::process::exit(1); }); - let storage = Arc::new( - service_storage::StorageService::new(config.clone()) - .await - .unwrap(), - ); - let upstream = Arc::new(service_upstream::UpstreamService::new()); + let storage = Arc::new(storage::StorageService::new(config.clone()).await.unwrap()); + let upstream = Arc::new(upstream::UpstreamService::new()); - let web_controller = Arc::new(controller_web::WebController::new()); - let zig_controller = Arc::new(controller_zig::ZigController::new( + let web_controller = Arc::new(WebController::default()); + let zig_controller = Arc::new(ZigController::new( config.clone(), storage.clone(), upstream.clone(), )); - let accept_logger = TraceLayer::new_for_http() + let trace_layer = TraceLayer::new_for_http() .make_span_with(|req: &http::Request<_>| { let request_id = req .headers() @@ -105,14 +112,15 @@ async fn main() { tracing::info_span!("http_request", request_id = %request_id) }) - .on_request(log_request) - .on_response(log_response) + .on_request(()) + .on_response(()) .on_failure(DefaultOnFailure::new().level(tracing::Level::ERROR)); let app = Router::new() .merge(web_controller.router()) .merge(zig_controller.router()) - .layer(accept_logger) + .layer(LoggingLayer) + .layer(trace_layer) .layer(request_id::PropagateRequestIdLayer::new( REQUEST_ID_HEADER.clone(), )) @@ -129,31 +137,68 @@ async fn main() { axum::serve(listener, app).await.unwrap(); } -fn log_request<B>(req: &http::Request<B>, _span: &tracing::Span) { - let headers = req.headers(); +#[derive(Clone)] +pub struct LoggingLayer; + +impl<S> tower::Layer<S> for LoggingLayer { + type Service = LoggingService<S>; + + fn layer(&self, inner: S) -> Self::Service { + LoggingService { inner } + } +} + +#[derive(Clone)] +pub struct LoggingService<S> { + inner: S, +} + +impl<S> tower::Service<Request<Body>> for LoggingService<S> +where + S: tower::Service<Request<Body>, Response = Response<Body>> + Clone + Send + 'static, + S::Future: Send, +{ + type Response = S::Response; + type Error = S::Error; + type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request<Body>) -> Self::Future { + Box::pin(log_request(self.inner.clone(), req)) + } +} + +async fn log_request<S>(mut inner: S, req: Request<Body>) -> Result<Response<Body>, S::Error> +where + S: tower::Service<Request<Body>, Response = Response<Body>>, +{ + let start = std::time::Instant::now(); + + let method = req.method().clone(); + let uri = req.uri().clone(); + let headers = req.headers(); let client_ip = headers .get("x-forwarded-for") .or_else(|| headers.get("x-real-ip")) - .and_then(|v| v.to_str().ok()); + .and_then(|v| v.to_str().ok()) + .map(String::from); let user_agent = headers .get(http::header::USER_AGENT) - .and_then(|v| v.to_str().ok()); + .and_then(|v| v.to_str().ok()) + .map(String::from); let referer = headers .get(http::header::REFERER) - .and_then(|v| v.to_str().ok()); + .and_then(|v| v.to_str().ok()) + .map(String::from); - info!( - method = %req.method(), - uri = %req.uri(), - client_ip, - user_agent, - referer, - "request started" - ); -} + let res = inner.call(req).await?; -fn log_response<B>(res: &http::Response<B>, latency: std::time::Duration, _span: &tracing::Span) { + let latency = start.elapsed(); + let status = res.status(); let headers = res.headers(); let content_length = headers .get(http::header::CONTENT_LENGTH) @@ -163,10 +208,161 @@ fn log_response<B>(res: &http::Response<B>, latency: std::time::Duration, _span: .and_then(|v| v.to_str().ok()); info!( - status = %res.status(), + method = %method, + uri = %uri, + status = %status, latency = ?latency, + client_ip = client_ip.as_deref(), + user_agent = user_agent.as_deref(), + referer = referer.as_deref(), content_length, content_type, - "request finished" + "request" ); + + Ok(res) +} + +/// Handles html pages rendering and static files +pub struct WebController { + jinja: minijinja::Environment<'static>, +} + +impl Default for WebController { + fn default() -> Self { + let mut jinja = minijinja::Environment::new(); + jinja.add_template("index", HTML).unwrap(); + + Self { jinja } + } } + +impl WebController { + pub fn router(self: Arc<Self>) -> Router { + Router::new() + .route("/index.css", routing::get(Self::styles)) + .route("/", routing::get(Self::index)) + .with_state(self) + } + + async fn index( + extract::State(controller): extract::State<Arc<Self>>, + ) -> Result<response::Response, http::StatusCode> { + let template = controller.jinja.get_template("index").unwrap(); + let rendered = template.render(minijinja::context! {}).unwrap(); + + let mut response = response::Html(rendered).into_response(); + response.headers_mut().insert( + http::header::CONTENT_SECURITY_POLICY, + http::HeaderValue::from_static( + "img-src 'self'; base-uri 'none'; font-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'; default-src 'self'; frame-ancestors 'none'", + ), + ); + + Ok(response) + } + + async fn styles() -> Result<axum_extra::response::Css<&'static str>, http::StatusCode> { + Ok(axum_extra::response::Css(CSS)) + } +} + +const HTML: &str = r#" +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + + <title>Earth PKG — tiny & opinionated packages mirror</title> + <link rel="stylesheet" href="index.css"> + </head> + + <body> + <h1>Earth PKG — tiny & opinionated packages mirror.</h1> + + <main> + <p>This site provides a proxy for downloading zig installation files and dependencies. + On the one hand, this reduces the load on the original project's site + and makes your infrastructure more reliable by adding redundancy.</p> + + <p>Read more about community mirrors in the <a href="https://ziglang.org/download/community-mirrors/">blog post</a>. + Information on how to deploy your own mirror is available + <a href="https://github.com/ziglang/www.ziglang.org/blob/main/MIRRORS.md">in the documentation</a>.</p> + + <h2>Direct usage:</h2> + + <p>For simplicity, you can use tools like <a href="https://github.com/prantlf/zigup">prantlf/zigup</a> and + <a href="https://github.com/mlugg/setup-zig">mlugg/setup-zig</a>.</p> + + <p> + To install manually: + <ol> + <li>download zig dist file:<br><code>wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz</code>;</li> + <li>download zig minisign file:<br><code>wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig</code>;</li> + <li>check archive integrity:<br><code>minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U</code>;</li> + <li>unpack archive:<br><code>tar -xf "zig-x86_64-linux-0.15.1.tar.xz"</code>;</li> + <li>check installed zig:<br><code>./zig-x86_64-linux-0.15.1/zig --version</code>.</li> + </ol> + + You can take actual minisign public key in <a href="https://ziglang.org/download/">download page</a>. + </p> + + <h2>Privacy policy</h2> + + <p>This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it.</p> + + <p>Since the mirror is hosted on hardware, we collect access logs to combat bots and brute-force attacks. + The logs are used for security purposes and load planning, are not shared with third parties, + and are deleted after 30 days.</p> + + <p>Third-party analytics systems are not used, same as client-side trackers.</p> + </main> + </body> +</html> +"#; + +const CSS: &str = " +:root { + font-size: 1.125rem; + line-height: 1.4; + font-family: + 'Alegreya Sans', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, + 'Open Sans', 'Helvetica Neue', sans-serif; +} + +html { + margin: 0; + padding: 0; +} + +body { + margin: 0 auto; + padding: 1.5em 2em; + max-width: 680px; +} + +code { + font-size: .75rem; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.2; + margin: 0; +} + +h1 { + font-size: 2.75rem; +} + +h1:first-child { + margin-top: 0; +} + +th { + text-align: start; +} +"; diff --git a/src/service_storage.rs b/src/storage.rs index f1f04b2..960afc5 100644 --- a/src/service_storage.rs +++ b/src/storage.rs @@ -38,7 +38,7 @@ use tokio::io::AsyncWriteExt; use tracing::{debug, instrument, warn}; use uuid::Uuid; -use super::service_config::ConfigService; +use super::config::ConfigService; const SQLITE_POOL_SIZE: u32 = 16; const INLINE_THRESHOLD: usize = 256 * 1024; // 256 KB diff --git a/src/service_upstream.rs b/src/upstream.rs index da25a7d..da25a7d 100644 --- a/src/service_upstream.rs +++ b/src/upstream.rs |
