aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--src/backends/go.rs128+42 −86
-rw-r--r--src/backends/mod.rs12+12 −0
-rw-r--r--src/backends/zig.rs128+42 −86
-rw-r--r--src/config.rs16+16 −0
-rw-r--r--src/controller_backend.rs95+95 −0
-rw-r--r--src/controller_web.rs (renamed from src/web.rs)0+0 −0
-rw-r--r--src/main.rs49+31 −18
7 files changed, 238 insertions, 190 deletions
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 <me@govorov.online>
// 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<String, ()> {
+ 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<config::ConfigService>,
- storage: Arc<storage::StorageService>,
- upstream: Arc<proxy::ProxyService>,
-}
-
-impl GoController {
- pub fn new(
- config: Arc<config::ConfigService>,
- storage: Arc<storage::StorageService>,
- upstream: Arc<proxy::ProxyService>,
- ) -> Self {
- Self {
- config,
- storage,
- upstream,
- }
- }
-
- pub fn router(self: Arc<Self>) -> Router {
- Router::new()
- .route("/go/{filename}", routing::get(Self::handle))
- .with_state(self)
- }
-
- async fn handle(
- extract::State(controller): extract::State<Arc<Self>>,
- extract::Path(filename): extract::Path<String>,
- ) -> Result<response::Response, http::StatusCode> {
- 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<String, ()>;
+}
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 <me@govorov.online>
// 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<String, ()> {
+ 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<config::ConfigService>,
- storage: Arc<storage::StorageService>,
- upstream: Arc<proxy::ProxyService>,
-}
-
-impl ZigController {
- pub fn new(
- config: Arc<config::ConfigService>,
- storage: Arc<storage::StorageService>,
- upstream: Arc<proxy::ProxyService>,
- ) -> Self {
- Self {
- config,
- storage,
- upstream,
- }
- }
-
- pub fn router(self: Arc<Self>) -> Router {
- Router::new()
- .route("/zig/{filename}", routing::get(Self::handle))
- .with_state(self)
- }
-
- async fn handle(
- extract::State(controller): extract::State<Arc<Self>>,
- extract::Path(filename): extract::Path<String>,
- ) -> Result<response::Response, http::StatusCode> {
- 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<Duration, D::Error>
where
D: serde::Deserializer<'de>,
@@ -241,6 +243,13 @@ pub struct TelemetryConfig {
pub otelcol: Option<OtelcolConfig>,
}
+#[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<ListenerConfig>,
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
--- /dev/null
+++ b/src/controller_backend.rs
@@ -0,0 +1,95 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
+// 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<B: Backend> {
+ backend: B,
+ storage: Arc<storage::StorageService>,
+ upstream: Arc<proxy::ProxyService>,
+}
+
+impl<B: Backend> BackendController<B> {
+ pub fn new(
+ backend: B,
+ storage: Arc<storage::StorageService>,
+ upstream: Arc<proxy::ProxyService>,
+ ) -> Self {
+ Self {
+ backend,
+ storage,
+ upstream,
+ }
+ }
+
+ pub fn router(self: Arc<Self>) -> Router {
+ Router::new()
+ .route("/{filename}", routing::get(Self::handle))
+ .with_state(self)
+ }
+
+ async fn handle(
+ extract::State(controller): extract::State<Arc<Self>>,
+ extract::Path(filename): extract::Path<String>,
+ ) -> Result<response::Response, http::StatusCode> {
+ 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/web.rs b/src/controller_web.rs
index 4866c15..4866c15 100644
--- a/src/web.rs
+++ b/src/controller_web.rs
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