From ea030e0dcdc9f282b3e3c96623e5e323b2a8b106 Mon Sep 17 00:00:00 2001
From: Nikolay Govorov
Date: Sat, 31 Jan 2026 07:33:13 +0000
Subject: Unifies backends
---
src/backends/go.rs | 128 ++++++------------
src/backends/mod.rs | 12 ++
src/backends/zig.rs | 128 ++++++------------
src/config.rs | 16 +++
src/controller_backend.rs | 95 +++++++++++++
src/controller_web.rs | 278 ++++++++++++++++++++++++++++++++++++++
src/main.rs | 49 ++++---
src/web.rs | 278 --------------------------------------
8 files changed, 516 insertions(+), 468 deletions(-)
create mode 100644 src/controller_backend.rs
create mode 100644 src/controller_web.rs
delete mode 100644 src/web.rs
diff --git a/src/backends/go.rs b/src/backends/go.rs
index 9a241c5..df94df8 100644
--- a/src/backends/go.rs
+++ b/src/backends/go.rs
@@ -1,15 +1,46 @@
// SPDX-FileCopyrightText: 2026 Nikolay Govorov
// SPDX-License-Identifier: AGPL-3.0-or-later
-use std::sync::Arc;
-
-use axum::{Router, body, extract, http, response, routing};
+use serde::Deserialize;
use thiserror::Error;
-use tracing::error;
-use crate::config;
-use crate::proxy;
-use crate::storage;
+use super::Backend;
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(default)]
+pub struct GoConfig {
+ pub enabled: bool,
+ pub upstream: String,
+}
+
+impl Default for GoConfig {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ upstream: String::from("https://dl.google.com/go"),
+ }
+ }
+}
+
+pub struct GoBackend {
+ config: GoConfig,
+ source: String,
+}
+
+impl GoBackend {
+ pub fn new(config: GoConfig, source: String) -> Self {
+ Self { config, source }
+ }
+}
+
+impl Backend for GoBackend {
+ const ID: &'static str = "go";
+
+ fn upstream_url(&self, filename: &str) -> Result {
+ let tarball = Tarball::parse(filename).map_err(|_| ())?;
+ Ok(tarball.upstream_url(&self.config.upstream, &self.source))
+ }
+}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Archive {
@@ -140,12 +171,8 @@ impl<'a> Tarball<'a> {
}
/// Builds the upstream URL for this tarball.
- pub fn upstream_url(&self, source: &str) -> String {
- // Use direct URL (go.dev/dl/ redirects to dl.google.com/go/)
- format!(
- "https://dl.google.com/go/{}?source={}",
- self.filename, source
- )
+ pub fn upstream_url(&self, upstream: &str, source: &str) -> String {
+ format!("{}/{}?source={}", upstream, self.filename, source)
}
}
@@ -168,77 +195,6 @@ fn parse_minor_with_release(s: &str) -> Result<(u32, ReleaseType), ParseError> {
Ok((minor, ReleaseType::Stable))
}
-pub struct GoController {
- config: Arc,
- storage: Arc,
- upstream: Arc,
-}
-
-impl GoController {
- pub fn new(
- config: Arc,
- storage: Arc,
- upstream: Arc,
- ) -> Self {
- Self {
- config,
- storage,
- upstream,
- }
- }
-
- pub fn router(self: Arc) -> Router {
- Router::new()
- .route("/go/{filename}", routing::get(Self::handle))
- .with_state(self)
- }
-
- async fn handle(
- extract::State(controller): extract::State>,
- extract::Path(filename): extract::Path,
- ) -> Result {
- let tarball = Tarball::parse(&filename).map_err(|_| http::StatusCode::NOT_FOUND)?;
- let url = tarball.upstream_url(controller.config.appname());
-
- match controller.storage.get("go", &filename).await {
- Ok(Some(entry)) => {
- return Ok(Self::build_response(
- http::StatusCode::OK,
- entry.file_bytes.0,
- ));
- }
- Ok(None) => {}
- Err(err) => {
- error!("failed get file from storage: {err}");
- return Err(http::StatusCode::INTERNAL_SERVER_ERROR);
- }
- }
-
- let entry = controller
- .upstream
- .fetch(proxy::DownloadRequest { url })
- .await?;
-
- match controller.storage.put("go", &filename, &entry.bytes).await {
- Ok(()) => {}
- Err(_) => {
- return Err(http::StatusCode::INTERNAL_SERVER_ERROR);
- }
- }
-
- Ok(Self::build_response(http::StatusCode::OK, entry.bytes))
- }
-
- fn build_response(status: http::StatusCode, bytes: bytes::Bytes) -> response::Response {
- response::Response::builder()
- .status(status)
- .header(http::header::CONTENT_TYPE, "application/octet-stream")
- .header(http::header::CONTENT_LENGTH, bytes.len())
- .body(body::Body::from(bytes))
- .unwrap()
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -333,10 +289,10 @@ mod tests {
#[test]
fn test_upstream_url() {
let t = Tarball::parse("go1.25.6.linux-amd64.tar.gz").unwrap();
- let url = t.upstream_url("zorian");
+ let url = t.upstream_url("https://dl.google.com/go", "zorian:test");
assert_eq!(
url,
- "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz?source=zorian"
+ "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz?source=zorian:test"
);
}
diff --git a/src/backends/mod.rs b/src/backends/mod.rs
index 910005c..97ac397 100644
--- a/src/backends/mod.rs
+++ b/src/backends/mod.rs
@@ -3,3 +3,15 @@
pub mod go;
pub mod zig;
+
+pub use go::{GoBackend, GoConfig};
+pub use zig::{ZigBackend, ZigConfig};
+
+/// Trait for backend-specific logic (parsing, URL building).
+pub trait Backend: Send + Sync + 'static {
+ /// Fixed unique identifier for storage
+ const ID: &'static str;
+
+ /// Validates filename and returns the upstream URL.
+ fn upstream_url(&self, filename: &str) -> Result;
+}
diff --git a/src/backends/zig.rs b/src/backends/zig.rs
index 9131511..424fd34 100644
--- a/src/backends/zig.rs
+++ b/src/backends/zig.rs
@@ -1,16 +1,47 @@
// SPDX-FileCopyrightText: 2026 Nikolay Govorov
// SPDX-License-Identifier: AGPL-3.0-or-later
-use std::sync::Arc;
-
-use axum::{Router, body, extract, http, response, routing};
use semver::Version;
+use serde::Deserialize;
use thiserror::Error;
-use tracing::error;
-use crate::config;
-use crate::proxy;
-use crate::storage;
+use super::Backend;
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(default)]
+pub struct ZigConfig {
+ pub enabled: bool,
+ pub upstream: String,
+}
+
+impl Default for ZigConfig {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ upstream: String::from("https://ziglang.org"),
+ }
+ }
+}
+
+pub struct ZigBackend {
+ config: ZigConfig,
+ source: String,
+}
+
+impl ZigBackend {
+ pub fn new(config: ZigConfig, source: String) -> Self {
+ Self { config, source }
+ }
+}
+
+impl Backend for ZigBackend {
+ const ID: &'static str = "zig";
+
+ fn upstream_url(&self, filename: &str) -> Result {
+ let tarball = Tarball::parse(filename).map_err(|_| ())?;
+ Ok(tarball.upstream_url(&self.config.upstream, &self.source))
+ }
+}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Archive {
@@ -132,89 +163,14 @@ impl<'a> Tarball<'a> {
}
/// Builds the upstream URL for this tarball.
- pub fn upstream_url(&self, source: &str) -> String {
+ pub fn upstream_url(&self, upstream: &str, source: &str) -> String {
if self.development {
- format!(
- "https://ziglang.org/builds/{}?source={}",
- self.filename, source
- )
+ format!("{}/builds/{}?source={}", upstream, self.filename, source)
} else {
format!(
- "https://ziglang.org/download/{}/{}?source={}",
- self.version, self.filename, source,
+ "{}/download/{}/{}?source={}",
+ upstream, self.version, self.filename, source,
)
}
}
}
-
-pub struct ZigController {
- config: Arc,
- storage: Arc,
- upstream: Arc,
-}
-
-impl ZigController {
- pub fn new(
- config: Arc,
- storage: Arc,
- upstream: Arc,
- ) -> Self {
- Self {
- config,
- storage,
- upstream,
- }
- }
-
- pub fn router(self: Arc) -> Router {
- Router::new()
- .route("/zig/{filename}", routing::get(Self::handle))
- .with_state(self)
- }
-
- async fn handle(
- extract::State(controller): extract::State>,
- extract::Path(filename): extract::Path,
- ) -> Result {
- let tarball = Tarball::parse(&filename).map_err(|_| http::StatusCode::NOT_FOUND)?;
- let url = tarball.upstream_url(controller.config.appname());
-
- match controller.storage.get("zig", &filename).await {
- Ok(Some(entry)) => {
- return Ok(Self::build_response(
- http::StatusCode::OK,
- entry.file_bytes.0,
- ));
- }
- Ok(None) => {}
- Err(err) => {
- error!("failed get file from storage: {err}");
- return Err(http::StatusCode::INTERNAL_SERVER_ERROR);
- }
- }
-
- let entry = controller
- .upstream
- .fetch(proxy::DownloadRequest { url })
- .await?;
-
- match controller.storage.put("zig", &filename, &entry.bytes).await {
- Ok(()) => {}
- Err(_) => {
- return Err(http::StatusCode::INTERNAL_SERVER_ERROR);
- }
- }
-
- Ok(Self::build_response(http::StatusCode::OK, entry.bytes))
- }
-
- fn build_response(status: http::StatusCode, bytes: bytes::Bytes) -> response::Response {
- // TODO: add etag
- response::Response::builder()
- .status(status)
- .header(http::header::CONTENT_TYPE, "application/octet-stream")
- .header(http::header::CONTENT_LENGTH, bytes.len())
- .body(body::Body::from(bytes))
- .unwrap()
- }
-}
diff --git a/src/config.rs b/src/config.rs
index 1e684bb..0fce8be 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -11,6 +11,8 @@ use bytesize::ByteSize;
use serde::Deserialize;
use thiserror::Error;
+use crate::backends::{GoConfig, ZigConfig};
+
fn deserialize_duration_secs<'de, D>(deserializer: D) -> Result
where
D: serde::Deserializer<'de>,
@@ -241,6 +243,13 @@ pub struct TelemetryConfig {
pub otelcol: Option,
}
+#[derive(Debug, Clone, Deserialize, Default)]
+#[serde(default)]
+pub struct BackendsConfig {
+ pub go: GoConfig,
+ pub zig: ZigConfig,
+}
+
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ConfigService {
@@ -249,6 +258,7 @@ pub struct ConfigService {
listen: Vec,
server: ServerConfig,
telemetry: TelemetryConfig,
+ backends: BackendsConfig,
}
impl Default for ConfigService {
fn default() -> Self {
@@ -258,6 +268,7 @@ impl Default for ConfigService {
server: ServerConfig::default(),
listen: vec![ListenerConfig::default()],
telemetry: TelemetryConfig::default(),
+ backends: BackendsConfig::default(),
}
}
}
@@ -334,6 +345,10 @@ impl ConfigService {
pub fn telemetry(&self) -> &TelemetryConfig {
&self.telemetry
}
+
+ pub fn backends(&self) -> &BackendsConfig {
+ &self.backends
+ }
}
#[cfg(test)]
@@ -350,6 +365,7 @@ impl ConfigService {
tls_key: None,
}],
telemetry: TelemetryConfig::default(),
+ backends: BackendsConfig::default(),
}
}
}
diff --git a/src/controller_backend.rs b/src/controller_backend.rs
new file mode 100644
index 0000000..ace9cdc
--- /dev/null
+++ b/src/controller_backend.rs
@@ -0,0 +1,95 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::sync::Arc;
+
+use axum::{Router, body, extract, http, response, routing};
+use tracing::error;
+
+use crate::backends::Backend;
+use crate::proxy;
+use crate::storage;
+
+/// Generic controller for backend HTTP handling.
+pub struct BackendController {
+ backend: B,
+ storage: Arc,
+ upstream: Arc,
+}
+
+impl BackendController {
+ pub fn new(
+ backend: B,
+ storage: Arc,
+ upstream: Arc,
+ ) -> Self {
+ Self {
+ backend,
+ storage,
+ upstream,
+ }
+ }
+
+ pub fn router(self: Arc) -> Router {
+ Router::new()
+ .route("/{filename}", routing::get(Self::handle))
+ .with_state(self)
+ }
+
+ async fn handle(
+ extract::State(controller): extract::State>,
+ extract::Path(filename): extract::Path,
+ ) -> Result {
+ let url = match controller.backend.upstream_url(&filename) {
+ Ok(url) => url,
+ Err(()) => {
+ error!(backend = B::ID, filename, "invalid filename");
+ return Err(http::StatusCode::NOT_FOUND);
+ }
+ };
+
+ match controller.storage.get(B::ID, &filename).await {
+ Ok(Some(entry)) => {
+ return Ok(Self::build_response(
+ http::StatusCode::OK,
+ entry.file_bytes.0,
+ ));
+ }
+ Ok(None) => {}
+ Err(err) => {
+ error!(
+ backend = B::ID,
+ filename, "failed to get file from storage: {err}"
+ );
+ return Err(http::StatusCode::INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ let entry = controller
+ .upstream
+ .fetch(proxy::DownloadRequest { url })
+ .await?;
+
+ match controller.storage.put(B::ID, &filename, &entry.bytes).await {
+ Ok(()) => {}
+ Err(err) => {
+ error!(
+ backend = B::ID,
+ filename, "failed to put file to storage: {err}"
+ );
+ return Err(http::StatusCode::INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ Ok(Self::build_response(http::StatusCode::OK, entry.bytes))
+ }
+
+ fn build_response(status: http::StatusCode, bytes: bytes::Bytes) -> response::Response {
+ response::Response::builder()
+ .status(status)
+ .header(http::header::CONTENT_TYPE, "application/octet-stream")
+ .header(http::header::CONTENT_LENGTH, bytes.len())
+ .body(body::Body::from(bytes))
+ .unwrap()
+ }
+}
diff --git a/src/controller_web.rs b/src/controller_web.rs
new file mode 100644
index 0000000..4866c15
--- /dev/null
+++ b/src/controller_web.rs
@@ -0,0 +1,278 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::future::Future;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll};
+
+use axum::body::Body;
+use axum::extract;
+use axum::http::{HeaderValue, Method, Request, Response, StatusCode, header};
+use axum::response;
+use chrono::{DateTime, Utc};
+use sqlx::types::chrono;
+use tower::{Layer, Service};
+use tracing::error;
+
+const CSP: &str = "default-src 'self'; base-uri 'none'; img-src 'self'; font-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'";
+
+/// 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) -> axum::Router {
+ axum::Router::new()
+ .route("/index.css", axum::routing::get(Self::styles))
+ .route("/", axum::routing::get(Self::index))
+ .layer(LastModifiedLayer {})
+ .layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
+ header::CACHE_CONTROL,
+ HeaderValue::from_static("no-cache"),
+ ))
+ .layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
+ header::CONTENT_SECURITY_POLICY,
+ HeaderValue::from_static(CSP),
+ ))
+ .with_state(self)
+ }
+
+ async fn index(
+ extract::State(controller): extract::State>,
+ ) -> Result, StatusCode> {
+ let tmpl = controller.jinja.get_template("index");
+
+ tmpl.and_then(|template| template.render(minijinja::context! {}))
+ .map(response::Html)
+ .map_err(|err| {
+ error!("html rendering failed: {err}");
+ StatusCode::INTERNAL_SERVER_ERROR
+ })
+ }
+
+ async fn styles() -> axum_extra::response::Css<&'static str> {
+ axum_extra::response::Css(CSS)
+ }
+}
+
+#[derive(Clone)]
+pub struct LastModifiedLayer {}
+impl Layer for LastModifiedLayer {
+ type Service = LastModifiedService;
+ fn layer(&self, inner: S) -> Self::Service {
+ LastModifiedService {
+ inner,
+
+ // Since the static file is packaged in a binary, we cache it across restarts.
+ // In the future, we can use the build time.
+ last_moidified: Utc::now(),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct LastModifiedService {
+ inner: S,
+ last_moidified: DateTime,
+}
+impl Service> for LastModifiedService
+where
+ S: Service, Response = Response> + Clone + Send + 'static,
+ S::Future: Send + 'static,
+ S::Error: Send + 'static,
+{
+ type Response = Response;
+ type Error = S::Error;
+ type Future = Pin> + Send>>;
+
+ fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {
+ self.inner.poll_ready(cx)
+ }
+
+ fn call(&mut self, req: Request) -> Self::Future {
+ let method = req.method().clone();
+ let headers = req.headers().clone();
+ let last_modified = self.last_moidified;
+
+ let mut inner = self.inner.clone();
+ Box::pin(async move {
+ let raw = last_modified
+ .format("%a, %d %b %Y %H:%M:%S GMT")
+ .to_string();
+ let lmh = HeaderValue::from_str(raw.as_str()).unwrap();
+
+ if (method == Method::GET || method == Method::HEAD)
+ && headers
+ .get(header::IF_MODIFIED_SINCE)
+ .and_then(|v| v.to_str().ok())
+ .and_then(parse_http_date)
+ .is_some_and(|ims| last_modified <= ims)
+ {
+ match Response::builder()
+ .status(StatusCode::NOT_MODIFIED)
+ .header(header::LAST_MODIFIED, lmh.clone())
+ .body(Body::empty())
+ {
+ Ok(resp) => {
+ return Ok(resp);
+ }
+ Err(err) => {
+ error!("build response: {err}")
+ }
+ }
+ }
+
+ let mut resp = inner.call(req).await?;
+ resp.headers_mut().insert(header::LAST_MODIFIED, lmh);
+ Ok(resp)
+ })
+ }
+}
+
+/// Parse HTTP-date (IMF-fixdate per RFC 7231) or RFC 2822
+fn parse_http_date(value: &str) -> Option> {
+ // IMF-fixdate: "Tue, 14 Jan 2026 12:34:56 GMT"
+ DateTime::parse_from_rfc2822(value.trim())
+ .map(|d| d.with_timezone(&Utc))
+ .ok()
+}
+
+const HTML: &str = r##"
+
+
+
+
+
+
+
+ Earth PKG — tiny & opinionated packages mirror
+
+
+
+
+ Zorian — tiny & opinionated packages mirror.
+
+
+ This site provides a caching proxy for downloading Zig and Go installation files.
+ It reduces load on upstream servers and makes your infrastructure more reliable by adding redundancy.
+
+ Zorian is open source software licensed under AGPL-3.0 .
+ Source code is available on GitHub .
+
+ Usage
+
+ Replace official download URLs with https://pkg.earth/{tool}/{filename}.
+ Files are cached automatically after the first download.
+
+
+
+ Read more about community mirrors in the blog post .
+ Information on how to deploy your own mirror is available
+ in the documentation .
+
+ For simplicity, you can use tools like prantlf/zigup and
+ mlugg/setup-zig .
+
+
+ To install manually:
+
+ download zig dist file:wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz;
+ download zig minisig file:wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig;
+ check archive integrity:minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U;
+ unpack archive:tar -xf "zig-x86_64-linux-0.15.1.tar.xz";
+ check installed zig:./zig-x86_64-linux-0.15.1/zig --version.
+
+ You can take actual minisig public key at ziglang.org/download .
+
+
+
+
+
+ To install manually:
+
+ download go dist file:wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz;
+ download go sha256 file:wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz.sha256;
+ check archive integrity:sha256sum -c go1.23.0.linux-amd64.tar.gz.sha256;
+ unpack archive:tar -xzf go1.23.0.linux-amd64.tar.gz;
+ check installed go:./go/bin/go version.
+
+ You can find available versions at go.dev/dl .
+
+
+ Privacy policy
+
+ This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it.
+
+ 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.
+
+ Third-party analytics systems are not used, same as client-side trackers.
+
+
+
+"##;
+
+const CSS: &str = r##"
+: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;
+}
+
+h3 a {
+ color: inherit;
+ text-decoration: none;
+}
+
+h3 a:hover {
+ text-decoration: underline;
+}
+"##;
diff --git a/src/main.rs b/src/main.rs
index 54b7246..955c4f4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,7 +6,9 @@ mod config;
mod proxy;
mod storage;
mod telemetry;
-mod web;
+
+mod controller_backend;
+mod controller_web;
use std::future::Future;
use std::net::{SocketAddr, TcpListener};
@@ -28,9 +30,9 @@ use tokio::signal;
use tracing::{error, info, trace};
use tracing_subscriber::registry::LookupSpan;
-use crate::backends::go::GoController;
-use crate::backends::zig::ZigController;
-use crate::web::WebController;
+use crate::backends::{GoBackend, ZigBackend};
+use crate::controller_backend::BackendController;
+use crate::controller_web::WebController;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const HELP: &str = "\
@@ -120,16 +122,8 @@ async fn main() {
let upstream = Arc::new(proxy::ProxyService::new());
let web_controller = Arc::new(WebController::default());
- let zig_controller = Arc::new(ZigController::new(
- config.clone(),
- storage.clone(),
- upstream.clone(),
- ));
- let go_controller = Arc::new(GoController::new(
- config.clone(),
- storage.clone(),
- upstream.clone(),
- ));
+ let source = format!("zorian:{}", config.appname());
+ let backends = config.backends();
const REQUEST_ID_HEADER: http::HeaderName = http::HeaderName::from_static("x-request-id");
@@ -225,10 +219,29 @@ async fn main() {
.finish()
.unwrap();
- let app = axum::Router::new()
- .merge(web_controller.router())
- .merge(zig_controller.router())
- .merge(go_controller.router())
+ let mut app = axum::Router::new().merge(web_controller.router());
+
+ if backends.zig.enabled {
+ let backend = ZigBackend::new(backends.zig.clone(), source.clone());
+ let ctrl = Arc::new(BackendController::new(
+ backend,
+ storage.clone(),
+ upstream.clone(),
+ ));
+ app = app.nest("/zig", ctrl.router());
+ }
+
+ if backends.go.enabled {
+ let backend = GoBackend::new(backends.go.clone(), source.clone());
+ let ctrl = Arc::new(BackendController::new(
+ backend,
+ storage.clone(),
+ upstream.clone(),
+ ));
+ app = app.nest("/go", ctrl.router());
+ }
+
+ let app = app
// Opt-in layers
.layer(tower_http::compression::CompressionLayer::new())
// request limits
diff --git a/src/web.rs b/src/web.rs
deleted file mode 100644
index 4866c15..0000000
--- a/src/web.rs
+++ /dev/null
@@ -1,278 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Nikolay Govorov
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-use std::future::Future;
-use std::pin::Pin;
-use std::sync::Arc;
-use std::task::{Context, Poll};
-
-use axum::body::Body;
-use axum::extract;
-use axum::http::{HeaderValue, Method, Request, Response, StatusCode, header};
-use axum::response;
-use chrono::{DateTime, Utc};
-use sqlx::types::chrono;
-use tower::{Layer, Service};
-use tracing::error;
-
-const CSP: &str = "default-src 'self'; base-uri 'none'; img-src 'self'; font-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'";
-
-/// 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) -> axum::Router {
- axum::Router::new()
- .route("/index.css", axum::routing::get(Self::styles))
- .route("/", axum::routing::get(Self::index))
- .layer(LastModifiedLayer {})
- .layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
- header::CACHE_CONTROL,
- HeaderValue::from_static("no-cache"),
- ))
- .layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
- header::CONTENT_SECURITY_POLICY,
- HeaderValue::from_static(CSP),
- ))
- .with_state(self)
- }
-
- async fn index(
- extract::State(controller): extract::State>,
- ) -> Result, StatusCode> {
- let tmpl = controller.jinja.get_template("index");
-
- tmpl.and_then(|template| template.render(minijinja::context! {}))
- .map(response::Html)
- .map_err(|err| {
- error!("html rendering failed: {err}");
- StatusCode::INTERNAL_SERVER_ERROR
- })
- }
-
- async fn styles() -> axum_extra::response::Css<&'static str> {
- axum_extra::response::Css(CSS)
- }
-}
-
-#[derive(Clone)]
-pub struct LastModifiedLayer {}
-impl Layer for LastModifiedLayer {
- type Service = LastModifiedService;
- fn layer(&self, inner: S) -> Self::Service {
- LastModifiedService {
- inner,
-
- // Since the static file is packaged in a binary, we cache it across restarts.
- // In the future, we can use the build time.
- last_moidified: Utc::now(),
- }
- }
-}
-
-#[derive(Clone)]
-pub struct LastModifiedService {
- inner: S,
- last_moidified: DateTime,
-}
-impl Service> for LastModifiedService
-where
- S: Service, Response = Response> + Clone + Send + 'static,
- S::Future: Send + 'static,
- S::Error: Send + 'static,
-{
- type Response = Response;
- type Error = S::Error;
- type Future = Pin> + Send>>;
-
- fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {
- self.inner.poll_ready(cx)
- }
-
- fn call(&mut self, req: Request) -> Self::Future {
- let method = req.method().clone();
- let headers = req.headers().clone();
- let last_modified = self.last_moidified;
-
- let mut inner = self.inner.clone();
- Box::pin(async move {
- let raw = last_modified
- .format("%a, %d %b %Y %H:%M:%S GMT")
- .to_string();
- let lmh = HeaderValue::from_str(raw.as_str()).unwrap();
-
- if (method == Method::GET || method == Method::HEAD)
- && headers
- .get(header::IF_MODIFIED_SINCE)
- .and_then(|v| v.to_str().ok())
- .and_then(parse_http_date)
- .is_some_and(|ims| last_modified <= ims)
- {
- match Response::builder()
- .status(StatusCode::NOT_MODIFIED)
- .header(header::LAST_MODIFIED, lmh.clone())
- .body(Body::empty())
- {
- Ok(resp) => {
- return Ok(resp);
- }
- Err(err) => {
- error!("build response: {err}")
- }
- }
- }
-
- let mut resp = inner.call(req).await?;
- resp.headers_mut().insert(header::LAST_MODIFIED, lmh);
- Ok(resp)
- })
- }
-}
-
-/// Parse HTTP-date (IMF-fixdate per RFC 7231) or RFC 2822
-fn parse_http_date(value: &str) -> Option> {
- // IMF-fixdate: "Tue, 14 Jan 2026 12:34:56 GMT"
- DateTime::parse_from_rfc2822(value.trim())
- .map(|d| d.with_timezone(&Utc))
- .ok()
-}
-
-const HTML: &str = r##"
-
-
-
-
-
-
-
- Earth PKG — tiny & opinionated packages mirror
-
-
-
-
- Zorian — tiny & opinionated packages mirror.
-
-
- This site provides a caching proxy for downloading Zig and Go installation files.
- It reduces load on upstream servers and makes your infrastructure more reliable by adding redundancy.
-
- Zorian is open source software licensed under AGPL-3.0 .
- Source code is available on GitHub .
-
- Usage
-
- Replace official download URLs with https://pkg.earth/{tool}/{filename}.
- Files are cached automatically after the first download.
-
-
-
- Read more about community mirrors in the blog post .
- Information on how to deploy your own mirror is available
- in the documentation .
-
- For simplicity, you can use tools like prantlf/zigup and
- mlugg/setup-zig .
-
-
- To install manually:
-
- download zig dist file:wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz;
- download zig minisig file:wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig;
- check archive integrity:minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U;
- unpack archive:tar -xf "zig-x86_64-linux-0.15.1.tar.xz";
- check installed zig:./zig-x86_64-linux-0.15.1/zig --version.
-
- You can take actual minisig public key at ziglang.org/download .
-
-
-
-
-
- To install manually:
-
- download go dist file:wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz;
- download go sha256 file:wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz.sha256;
- check archive integrity:sha256sum -c go1.23.0.linux-amd64.tar.gz.sha256;
- unpack archive:tar -xzf go1.23.0.linux-amd64.tar.gz;
- check installed go:./go/bin/go version.
-
- You can find available versions at go.dev/dl .
-
-
- Privacy policy
-
- This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it.
-
- 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.
-
- Third-party analytics systems are not used, same as client-side trackers.
-
-
-
-"##;
-
-const CSS: &str = r##"
-: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;
-}
-
-h3 a {
- color: inherit;
- text-decoration: none;
-}
-
-h3 a:hover {
- text-decoration: underline;
-}
-"##;
--
Gilti