aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikolay Govorov <me@govorov.online>2026-02-07 23:58:00 +0000
committerNikolay Govorov <me@govorov.online>2026-02-08 00:04:13 +0000
commit3c01f4de09d01578d82f6fdb7a0afbcaf6268193 (patch)
treed6289b394f2287d52aa145b2c1f6dc65fc4aa114
parenta7b9c0ac6d5f23e7a948032df8ae7e820d73b8b3 (diff)
downloadtar
tar.gz
tar.bz2
tar.lz
tar.xz
tar.zst
zip
Add favicon, setup etag for static, drop licenses for another targets
Diffstat
-rw-r--r--Cargo.lock91+10 −81
-rw-r--r--Cargo.toml5+2 −3
-rw-r--r--REUSE.toml5+5 −0
-rw-r--r--crates/base/Cargo.toml20+20 −0
-rw-r--r--crates/base/src/lib.rs31+31 −0
-rw-r--r--crates/base/src/serde.rs (renamed from crates/zorian/src/utils.rs)48+24 −24
-rw-r--r--crates/repos/Cargo.toml3+2 −1
-rw-r--r--crates/repos/src/lib.rs10+5 −5
-rw-r--r--crates/zorian/Cargo.toml3+1 −2
-rw-r--r--crates/zorian/src/assets/apple-touch-icon.pngbin0 -> 715 bytes
-rw-r--r--crates/zorian/src/assets/favicon-192.pngbin0 -> 715 bytes
-rw-r--r--crates/zorian/src/assets/favicon-512.pngbin0 -> 2329 bytes
-rw-r--r--crates/zorian/src/assets/favicon.icobin0 -> 32038 bytes
-rw-r--r--crates/zorian/src/assets/favicon.svg4+4 −0
-rw-r--r--crates/zorian/src/assets/manifest.webmanifest8+8 −0
-rw-r--r--crates/zorian/src/config.rs2+1 −1
-rw-r--r--crates/zorian/src/controller_backend.rs5+2 −3
-rw-r--r--crates/zorian/src/controller_web.rs508+260 −248
-rw-r--r--crates/zorian/src/main.rs1+0 −1
-rw-r--r--deny.toml5+3 −2
-rw-r--r--pkg/logo.svg30+30 −0
-rw-r--r--xtask/build.rs9+9 −0
-rw-r--r--xtask/src/licenses.rs2+1 −1
23 files changed, 418 insertions, 372 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 219520b..e608820 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -193,6 +193,14 @@ dependencies = [
]
[[package]]
+name = "base"
+version = "0.0.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1371,16 +1379,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
-name = "mime_guess"
-version = "2.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
-dependencies = [
- "mime",
- "unicase",
-]
-
-[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2021,8 +2019,8 @@ name = "repos"
version = "0.0.0"
dependencies = [
"async-trait",
+ "base",
"bytes",
- "mime_guess",
"proptest",
"semver",
"serde",
@@ -2102,40 +2100,6 @@ dependencies = [
]
[[package]]
-name = "rust-embed"
-version = "8.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
-dependencies = [
- "rust-embed-impl",
- "rust-embed-utils",
- "walkdir",
-]
-
-[[package]]
-name = "rust-embed-impl"
-version = "8.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
-dependencies = [
- "proc-macro2",
- "quote",
- "rust-embed-utils",
- "syn",
- "walkdir",
-]
-
-[[package]]
-name = "rust-embed-utils"
-version = "8.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
-dependencies = [
- "sha2",
- "walkdir",
-]
-
-[[package]]
name = "rustix"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2220,15 +2184,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
-name = "same-file"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
name = "schannel"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3196,12 +3151,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
-name = "unicase"
-version = "2.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
-
-[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3293,16 +3242,6 @@ dependencies = [
]
[[package]]
-name = "walkdir"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
-dependencies = [
- "same-file",
- "winapi-util",
-]
-
-[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3438,15 +3377,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
-name = "winapi-util"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
-dependencies = [
- "windows-sys 0.61.2",
-]
-
-[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3879,6 +3809,7 @@ dependencies = [
"async-trait",
"axum",
"axum-server",
+ "base",
"bytes",
"bytesize",
"chrono",
@@ -3889,14 +3820,12 @@ dependencies = [
"hyper-tls",
"hyper-util",
"maud",
- "mime_guess",
"opentelemetry",
"opentelemetry-appender-tracing",
"opentelemetry-otlp",
"opentelemetry-semantic-conventions",
"opentelemetry_sdk",
"repos",
- "rust-embed",
"rustls",
"sd-notify",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index daef133..19d2306 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
[workspace]
-members = ["crates/repos", "crates/zorian", "xtask"]
+members = ["crates/*", "xtask"]
resolver = "3"
[workspace.package]
@@ -16,6 +16,7 @@ repository = "https://github.com/mrdimidium/Zorian"
[workspace.dependencies]
# local dependencies
+base = { path = "crates/base" }
repos = { path = "crates/repos" }
# external
@@ -32,8 +33,6 @@ hyper = "1.8"
hyper-tls = "0.6"
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2", "tokio"] }
maud = { version = "0.27", features = ["axum"] }
-mime_guess = "2.0"
-rust-embed = "8.11"
rustls = { version = "0.23", default-features = false, features = [
"ring",
"std",
diff --git a/REUSE.toml b/REUSE.toml
index d77fe9a..86e2035 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -12,6 +12,11 @@ SPDX-License-Identifier = "AGPL-3.0-or-later"
[[annotations]]
path = [
+ "pkg/*.svg",
+ "crates/zorian/src/assets/*.svg",
+ "crates/zorian/src/assets/*.ico",
+ "crates/zorian/src/assets/*.png",
+ "crates/zorian/src/assets/manifest.webmanifest",
"crates/zorian/src/assets/jetbrainsmono/OFL.txt",
"crates/zorian/src/assets/jetbrainsmono/*.woff2",
]
diff --git a/crates/base/Cargo.toml b/crates/base/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/crates/base/Cargo.toml
@@ -0,0 +1,20 @@
+# SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+[package]
+name = "base"
+version = "0.0.0"
+description = "Shared types and utilities"
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+publish.workspace = true
+homepage.workspace = true
+repository.workspace = true
+
+[dependencies]
+serde.workspace = true
+
+[dev-dependencies]
+serde_json.workspace = true
diff --git a/crates/base/src/lib.rs b/crates/base/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/crates/base/src/lib.rs
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pub mod serde;
+
+#[derive(Debug, Clone, Copy)]
+pub enum ContentType {
+ TextPlain,
+ OctetStream,
+ ImageXIcon,
+ ImagePng,
+ ImageSvgXml,
+ TextCss,
+ FontWoff2,
+ ApplicationManifestJson,
+}
+
+impl ContentType {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::TextPlain => "text/plain",
+ Self::OctetStream => "application/octet-stream",
+ Self::ImageXIcon => "image/x-icon",
+ Self::ImagePng => "image/png",
+ Self::ImageSvgXml => "image/svg+xml",
+ Self::TextCss => "text/css",
+ Self::FontWoff2 => "font/woff2",
+ Self::ApplicationManifestJson => "application/manifest+json",
+ }
+ }
+}
diff --git a/crates/zorian/src/utils.rs b/crates/base/src/serde.rs
index 0cf2e73..7897b02 100644
--- a/crates/zorian/src/utils.rs
+++ b/crates/base/src/serde.rs
@@ -4,21 +4,41 @@
use std::net::SocketAddr;
use std::time::Duration;
-use serde::de::Deserialize;
+use ::serde::de::Deserialize;
/// Deserializes a duration from seconds (u64).
pub fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
- D: serde::Deserializer<'de>,
+ D: ::serde::Deserializer<'de>,
{
let secs = u64::deserialize(deserializer)?;
Ok(Duration::from_secs(secs))
}
+/// Deserializes a SocketAddr from a string.
+pub fn deserialize_listener_addr<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error>
+where
+ D: ::serde::Deserializer<'de>,
+{
+ let raw = String::deserialize(deserializer)?;
+ let raw = raw.trim();
+
+ // Hostnames are not supported, but localhost shorthands are useful
+ if let Some(port) = raw.strip_prefix("localhost:") {
+ return port
+ .parse::<u16>()
+ .map(|port| SocketAddr::new(std::net::Ipv4Addr::LOCALHOST.into(), port))
+ .map_err(|err| ::serde::de::Error::custom(format!("invalid port in '{raw}': {err}")));
+ }
+
+ raw.parse::<SocketAddr>()
+ .map_err(|err| ::serde::de::Error::custom(format!("invalid address '{raw}': {err}")))
+}
+
#[cfg(test)]
mod deserialize_duration_tests {
use super::*;
- use serde::Deserialize;
+ use ::serde::Deserialize;
#[derive(Debug, Deserialize)]
struct DurationWrapper {
@@ -39,30 +59,10 @@ mod deserialize_duration_tests {
}
}
-/// Deserializes a SocketAddr from a string.
-pub fn deserialize_listener_addr<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error>
-where
- D: serde::Deserializer<'de>,
-{
- let raw = String::deserialize(deserializer)?;
- let raw = raw.trim();
-
- // Hostnames are not supported, but localhost shorthands are useful
- if let Some(port) = raw.strip_prefix("localhost:") {
- return port
- .parse::<u16>()
- .map(|port| SocketAddr::new(std::net::Ipv4Addr::LOCALHOST.into(), port))
- .map_err(|err| serde::de::Error::custom(format!("invalid port in '{raw}': {err}")));
- }
-
- raw.parse::<SocketAddr>()
- .map_err(|err| serde::de::Error::custom(format!("invalid address '{raw}': {err}",)))
-}
-
#[cfg(test)]
mod deserialize_listener_addr_tests {
use super::*;
- use serde::Deserialize;
+ use ::serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AddrWrapper {
diff --git a/crates/repos/Cargo.toml b/crates/repos/Cargo.toml
index 9924851..38bfc2e 100644
--- a/crates/repos/Cargo.toml
+++ b/crates/repos/Cargo.toml
@@ -14,9 +14,10 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
+base.workspace = true
+
async-trait.workspace = true
bytes.workspace = true
-mime_guess.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
diff --git a/crates/repos/src/lib.rs b/crates/repos/src/lib.rs
index d6344f7..14fd3a9 100644
--- a/crates/repos/src/lib.rs
+++ b/crates/repos/src/lib.rs
@@ -10,11 +10,11 @@ use std::time::Duration;
use async_trait::async_trait;
use bytes::Bytes;
-use mime_guess::mime;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use thiserror::Error;
use url::Url;
+pub use base::ContentType;
pub use go::{GoBackend, GoConfig};
pub use zig::{ZigBackend, ZigConfig};
@@ -151,10 +151,10 @@ pub enum VersionType {
/// Result of resolving a file request.
pub enum ResolvedFile {
/// Proxy request to upstream URL
- Upstream { mime: mime::Mime, url: Url },
+ Upstream { mime: ContentType, url: Url },
/// Return content directly
Content {
- mime: mime::Mime,
+ mime: ContentType,
data: bytes::Bytes,
},
}
@@ -460,9 +460,9 @@ impl<S: BackendSpec> Backend<S> {
let is_checksum_or_sig = parsed.is_sha256() || parsed.is_signature();
let mime = if is_checksum_or_sig {
- mime::TEXT_PLAIN
+ ContentType::TextPlain
} else {
- mime::APPLICATION_OCTET_STREAM
+ ContentType::OctetStream
};
let url = parsed.upstream_url(&self.config, &self.source)?;
diff --git a/crates/zorian/Cargo.toml b/crates/zorian/Cargo.toml
index d3ac7b9..4317d22 100644
--- a/crates/zorian/Cargo.toml
+++ b/crates/zorian/Cargo.toml
@@ -14,6 +14,7 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
+base.workspace = true
repos.workspace = true
async-trait.workspace = true
@@ -29,8 +30,6 @@ hyper.workspace = true
hyper-tls.workspace = true
hyper-util.workspace = true
maud.workspace = true
-mime_guess.workspace = true
-rust-embed.workspace = true
rustls.workspace = true
serde.workspace = true
serde_json.workspace = true
diff --git a/crates/zorian/src/assets/apple-touch-icon.png b/crates/zorian/src/assets/apple-touch-icon.png
new file mode 100644
--- /dev/null
+++ b/crates/zorian/src/assets/apple-touch-icon.png
Binary files differ
diff --git a/crates/zorian/src/assets/favicon-192.png b/crates/zorian/src/assets/favicon-192.png
new file mode 100644
--- /dev/null
+++ b/crates/zorian/src/assets/favicon-192.png
Binary files differ
diff --git a/crates/zorian/src/assets/favicon-512.png b/crates/zorian/src/assets/favicon-512.png
new file mode 100644
--- /dev/null
+++ b/crates/zorian/src/assets/favicon-512.png
Binary files differ
diff --git a/crates/zorian/src/assets/favicon.ico b/crates/zorian/src/assets/favicon.ico
new file mode 100644
--- /dev/null
+++ b/crates/zorian/src/assets/favicon.ico
Binary files differ
diff --git a/crates/zorian/src/assets/favicon.svg b/crates/zorian/src/assets/favicon.svg
new file mode 100644
--- /dev/null
+++ b/crates/zorian/src/assets/favicon.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" fill="#f7a41d" viewBox="-150 -150 800 800">
+ <path d="M100 0v100H0v400h500V100H400V0H100m0 200h100v100H100zm200 0h100v100H300z"/>
+</svg>
diff --git a/crates/zorian/src/assets/manifest.webmanifest b/crates/zorian/src/assets/manifest.webmanifest
new file mode 100644
--- /dev/null
+++ b/crates/zorian/src/assets/manifest.webmanifest
@@ -0,0 +1,8 @@
+{
+ "name": "Zorian",
+ "icons": [
+ { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
+ { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" },
+ { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" }
+ ]
+}
diff --git a/crates/zorian/src/config.rs b/crates/zorian/src/config.rs
index c05ce7e..5719d22 100644
--- a/crates/zorian/src/config.rs
+++ b/crates/zorian/src/config.rs
@@ -12,7 +12,7 @@ use serde::Deserialize;
use thiserror::Error;
use tracing::info;
-use crate::utils::{deserialize_duration, deserialize_listener_addr};
+use base::serde::{deserialize_duration, deserialize_listener_addr};
use repos::{GoConfig, ZigConfig};
#[derive(Debug, Error)]
diff --git a/crates/zorian/src/controller_backend.rs b/crates/zorian/src/controller_backend.rs
index 70de572..d14d93b 100644
--- a/crates/zorian/src/controller_backend.rs
+++ b/crates/zorian/src/controller_backend.rs
@@ -4,7 +4,6 @@
use std::sync::Arc;
use axum::{Router, body, extract, http, response, routing};
-use mime_guess::mime;
use tracing::error;
use crate::proxy;
@@ -100,11 +99,11 @@ impl<S: BackendSpec> BackendController<S> {
fn build_response(
status: http::StatusCode,
bytes: bytes::Bytes,
- mime: mime::Mime,
+ mime: repos::ContentType,
) -> response::Response {
response::Response::builder()
.status(status)
- .header(http::header::CONTENT_TYPE, mime.as_ref())
+ .header(http::header::CONTENT_TYPE, mime.as_str())
.header(http::header::CONTENT_LENGTH, bytes.len())
.body(body::Body::from(bytes))
.unwrap()
diff --git a/crates/zorian/src/controller_web.rs b/crates/zorian/src/controller_web.rs
index 11391c8..8e753de 100644
--- a/crates/zorian/src/controller_web.rs
+++ b/crates/zorian/src/controller_web.rs
@@ -9,19 +9,48 @@ use std::task::{Context, Poll};
use axum::body::Body;
use axum::http::{HeaderValue, Method, Request, Response, StatusCode, header};
use axum::{extract, routing};
-use chrono::{DateTime, Utc};
use maud::{Markup, html};
-use rust_embed::Embed;
use serde::Deserialize;
-use sqlx::types::chrono;
use tower::{Layer, Service};
use tracing::error;
+use base::ContentType;
use repos::{GoBackend, ZigBackend};
-#[derive(Embed)]
-#[folder = "src/assets/"]
-struct Assets;
+fn get_asset(path: &str) -> Option<(&'static [u8], ContentType)> {
+ Some(match path {
+ "base.css" => (include_bytes!("assets/base.css"), ContentType::TextCss),
+ "favicon.svg" => (
+ include_bytes!("assets/favicon.svg"),
+ ContentType::ImageSvgXml,
+ ),
+ "favicon.ico" => (
+ include_bytes!("assets/favicon.ico"),
+ ContentType::ImageXIcon,
+ ),
+ "favicon-192.png" | "apple-touch-icon.png" => (
+ include_bytes!("assets/favicon-192.png"),
+ ContentType::ImagePng,
+ ),
+ "favicon-512.png" => (
+ include_bytes!("assets/favicon-512.png"),
+ ContentType::ImagePng,
+ ),
+ "manifest.webmanifest" => (
+ include_bytes!("assets/manifest.webmanifest"),
+ ContentType::ApplicationManifestJson,
+ ),
+ "jetbrainsmono/JetBrainsMono[wght].woff2" => (
+ include_bytes!("assets/jetbrainsmono/JetBrainsMono[wght].woff2"),
+ ContentType::FontWoff2,
+ ),
+ "jetbrainsmono/JetBrainsMono-Italic[wght].woff2" => (
+ include_bytes!("assets/jetbrainsmono/JetBrainsMono-Italic[wght].woff2"),
+ ContentType::FontWoff2,
+ ),
+ _ => return None,
+ })
+}
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'";
@@ -54,6 +83,7 @@ struct LicenseUsedBy {
}
#[derive(Deserialize)]
+#[allow(dead_code)]
struct LicenseCrate {
name: String,
version: String,
@@ -79,52 +109,35 @@ impl WebController {
impl WebController {
pub fn router(self: Arc<Self>) -> axum::Router {
- let pages = axum::Router::new()
+ axum::Router::new()
.route("/", axum::routing::get(Self::index))
- .route("/licenses", axum::routing::get(Self::licenses))
+ .route("/{*path}", routing::get(Self::assets))
+ .route("/about/licenses", axum::routing::get(Self::licenses))
+ .layer(CacheLayer)
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
- header::CACHE_CONTROL,
- HeaderValue::from_static("no-cache"),
- ))
- .with_state(self.clone());
-
- let assets = axum::Router::new()
- .route("/assets/{*path}", routing::get(Self::assets))
- .layer(LastModifiedLayer {});
-
- axum::Router::new().merge(pages).merge(assets).layer(
- tower_http::set_header::SetResponseHeaderLayer::overriding(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_static(CSP),
- ),
- )
+ ))
+ .with_state(self.clone())
}
async fn assets(extract::Path(path): extract::Path<String>) -> Response<Body> {
- match Assets::get(&path) {
- Some(file) => {
- let mime = mime_guess::from_path(&path).first_or_octet_stream();
- let is_font = matches!(
- path.rsplit('.').next(),
- Some("ttf" | "otf" | "woff" | "woff2" | "eot")
- );
-
- let mut builder = Response::builder()
- .status(StatusCode::OK)
- .header(header::CONTENT_TYPE, mime.as_ref());
-
- if is_font {
- builder = builder
- .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable");
- }
-
- builder.body(Body::from(file.data.into_owned())).unwrap()
- }
- None => Response::builder()
+ let Some((data, content_type)) = get_asset(&path) else {
+ return Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())
- .unwrap(),
+ .unwrap();
+ };
+
+ let mut builder = Response::builder()
+ .status(StatusCode::OK)
+ .header(header::CONTENT_TYPE, content_type.as_str());
+
+ if matches!(content_type, ContentType::FontWoff2) {
+ builder = builder.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable");
}
+
+ builder.body(Body::from(data)).unwrap()
}
async fn index(extract::State(ctrl): extract::State<Arc<Self>>) -> Markup {
@@ -152,81 +165,69 @@ impl WebController {
Vec::new()
};
- html! {
- (maud::DOCTYPE)
-
- html lang="en" {
- meta charset="UTF-8";
- meta name="viewport" content="width=device-width,initial-scale=1";
- meta http-equiv="X-UA-Compatible" content="ie=edge";
-
- link rel="stylesheet" href="assets/base.css";
- title { "Earth PKG — tiny & opinionated packages mirror" }
- }
-
- body {
+ WebController::layout(
+ "Zorian — tiny & opinionated packages mirror",
+ html! {
h1 { "Zorian — tiny & opinionated packages mirror." }
- main {
- p {
- r#"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."#
- }
-
- p {
- "Zorian is open source software licensed under " a href="https://www.gnu.org/licenses/agpl-3.0.html" { "AGPL-3.0" } ". "
- "Source code is available on " a href="https://github.com/mrdimidium/zorian" { "GitHub" } ". "
- "A list of dependency licenses " a href="/licenses" { "is available" } "."
- }
+ p {
+ r#"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."#
+ }
- h2 { "Usage" }
+ p {
+ "Zorian is open source software licensed under " a href="https://www.gnu.org/licenses/agpl-3.0.html" { "AGPL-3.0" } ". "
+ "Source code is available on " a href="https://github.com/mrdimidium/zorian" { "GitHub" } ". "
+ "A list of dependency licenses " a href="/about/licenses" { "is available" } "."
+ }
- p {
- "Replace official download URLs with " code { "https://pkg.earth/{tool}/{filename}" } ". "
- "Files are cached automatically after the first download."
- }
+ h2 { "Usage" }
- h3 id="zig" {
- a href="#zig" { "Zig" }
- }
+ p {
+ "Replace official download URLs with " code { "https://pkg.earth/{tool}/{filename}" } ". "
+ "Files are cached automatically after the first download."
+ }
- @if !zig_versions.is_empty() {
- details {
- summary { "Available versions (" (zig_versions.len()) ")" }
+ h3 id="zig" {
+ a href="#zig" { "Zig" }
+ }
- p { "You can take actual minisig public key at " a href="https://ziglang.org/download/" { "ziglang.org/download" } "." }
- table {
- thead {
- tr {
- th { "Version" }
- th { "Date" }
- th { "Docs" }
- th { "Targets" }
- }
+ @if !zig_versions.is_empty() {
+ details {
+ summary { "Available versions (" (zig_versions.len()) ")" }
+
+ p { "You can take actual minisig public key at " a href="https://ziglang.org/download/" { "ziglang.org/download" } "." }
+ table {
+ thead {
+ tr {
+ th { "Version" }
+ th { "Date" }
+ th { "Docs" }
+ th { "Targets" }
}
- tbody {
- @for v in zig_versions.iter().rev() {
- tr {
- td { (v.version) }
- td { (v.meta.date.as_deref().unwrap_or("-")) }
- td {
- @if let Some(ref url) = v.meta.docs {
- a href=(url) { "docs" }
- }
- " "
- @if let Some(ref url) = v.meta.std_docs {
- a href=(url) { "std" }
- }
- " "
- @if let Some(ref url) = v.meta.notes {
- a href=(url) { "notes" }
- }
+ }
+ tbody {
+ @for v in zig_versions.iter().rev() {
+ tr {
+ td { (v.version) }
+ td { (v.meta.date.as_deref().unwrap_or("-")) }
+ td {
+ @if let Some(ref url) = v.meta.docs {
+ a href=(url) { "docs" }
+ }
+ " "
+ @if let Some(ref url) = v.meta.std_docs {
+ a href=(url) { "std" }
}
- td {
- @for file in &v.files {
- a href=(format!("/zig/{}", file.filename)) { (&file.meta.target) }
- " "
- }
+ " "
+ @if let Some(ref url) = v.meta.notes {
+ a href=(url) { "notes" }
+ }
+ }
+ td {
+ @for file in &v.files {
+ a href=(format!("/zig/{}", file.filename)) { (&file.meta.target) }
+ " "
}
}
}
@@ -234,53 +235,53 @@ impl WebController {
}
}
}
+ }
- p {
- "Read more about community mirrors in the " a href="https://ziglang.org/download/community-mirrors/" { "blog post" } ". "
- "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" } "."
- }
+ p {
+ "Read more about community mirrors in the " a href="https://ziglang.org/download/community-mirrors/" { "blog post" } ". "
+ "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" } "."
+ }
- p {
- "For simplicity, you can use tools like " a href="https://github.com/prantlf/zigup" { "prantlf/zigup" }
- " and " a href="https://github.com/mlugg/setup-zig" { "mlugg/setup-zig" } "."
- }
+ p {
+ "For simplicity, you can use tools like " a href="https://github.com/prantlf/zigup" { "prantlf/zigup" }
+ " and " a href="https://github.com/mlugg/setup-zig" { "mlugg/setup-zig" } "."
+ }
- 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" } ";" }
- li { "download zig minisig file:" br; code { "wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig" } ";" }
- li { "check archive integrity:" br; code { "minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U" } ";" }
- li { "unpack archive:" br; code { "tar -xf 'zig-x86_64-linux-0.15.1.tar.xz'" } ";" }
- li { "check installed zig:" br; code { "./zig-x86_64-linux-0.15.1/zig --version" } ";" }
- }
+ 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" } ";" }
+ li { "download zig minisig file:" br; code { "wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig" } ";" }
+ li { "check archive integrity:" br; code { "minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U" } ";" }
+ li { "unpack archive:" br; code { "tar -xf 'zig-x86_64-linux-0.15.1.tar.xz'" } ";" }
+ li { "check installed zig:" br; code { "./zig-x86_64-linux-0.15.1/zig --version" } ";" }
}
+ }
- h3 id="go" { a href="#go" { "Go" } }
+ h3 id="go" { a href="#go" { "Go" } }
- @if !go_versions.is_empty() {
- details {
- summary { "Available versions (" (go_versions.len()) ")" }
+ @if !go_versions.is_empty() {
+ details {
+ summary { "Available versions (" (go_versions.len()) ")" }
- p { "You can find available versions at " a href="https://go.dev/dl/" { "go.dev/dl" } "." }
- table {
- thead {
- tr {
- th { "Version" }
- th { "Stable" }
- th { "Files" }
- }
+ p { "You can find available versions at " a href="https://go.dev/dl/" { "go.dev/dl" } "." }
+ table {
+ thead {
+ tr {
+ th { "Version" }
+ th { "Stable" }
+ th { "Files" }
}
- tbody {
- @for v in go_versions.iter().rev() {
- tr {
- td { (v.version) }
- td { @if v.meta.stable { "✓" } @else { "" } }
- td {
- @for file in &v.files {
- a href=(format!("/go/{}", file.filename)) { (file.filename) }
- " "
- }
+ }
+ tbody {
+ @for v in go_versions.iter().rev() {
+ tr {
+ td { (v.version) }
+ td { @if v.meta.stable { "✓" } @else { "" } }
+ td {
+ @for file in &v.files {
+ a href=(format!("/go/{}", file.filename)) { (file.filename) }
+ " "
}
}
}
@@ -288,85 +289,99 @@ impl WebController {
}
}
}
+ }
- p { "To install manually:" }
+ p { "To install manually:" }
- ol {
- li { "download go dist file:" br; code { "wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz" } ";" }
- li { "download go sha256 file:" br; code { "wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz.sha256" } ";" }
- li { "check archive integrity:" br; code { "sha256sum -c go1.23.0.linux-amd64.tar.gz.sha256" } ";" }
- li { "unpack archive:" br; code { "tar -xzf go1.23.0.linux-amd64.tar.gz" } ";" }
- li { "check installed go:" br; code { "./go/bin/go version" } ";" }
- }
+ ol {
+ li { "download go dist file:" br; code { "wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz" } ";" }
+ li { "download go sha256 file:" br; code { "wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz.sha256" } ";" }
+ li { "check archive integrity:" br; code { "sha256sum -c go1.23.0.linux-amd64.tar.gz.sha256" } ";" }
+ li { "unpack archive:" br; code { "tar -xzf go1.23.0.linux-amd64.tar.gz" } ";" }
+ li { "check installed go:" br; code { "./go/bin/go version" } ";" }
+ }
- h2 { "Privacy policy" }
+ h2 { "Privacy policy" }
- p { "This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it." }
+ p { "This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it." }
- p { r#"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 { r#"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 { "Third-party analytics systems are not used, same as client-side trackers." }
- }
- }
- }
+ p { "Third-party analytics systems are not used, same as client-side trackers." }
+ },
+ )
}
async fn licenses() -> Markup {
let data = &*LICENSES;
+ WebController::layout(
+ "Third Party Licenses",
+ html! {
+ h1 { "Third Party Licenses" }
+
+ p { "This page lists the licenses of the projects used in Zorian." }
+
+ h2 { "Overview of licenses:" }
+ ul {
+ @for ov in &data.overview {
+ li { a href=(format!("#{}", ov.id)) { (&ov.name) } " (" (ov.count) ")" }
+ }
+ }
+
+ h2 { "All license text:" }
+ @for (i, lic) in data.licenses.iter().enumerate() {
+ div.license {
+ @if lic.first_of_kind {
+ h3 id=(&lic.id) { (&lic.name) }
+ }
+ h4 id=(format!("{}-{}", lic.id, i)) { (&lic.name) }
+
+ p { "Used by:" }
+
+ ul .license-used-by {
+ @for usage in &lic.used_by {
+ li {
+ a href=(
+ usage.crate_.repository.as_deref()
+ .unwrap_or(&format!("https://crates.io/crates/{}", usage.crate_.name))
+ ) {
+ (&usage.crate_.name) "@" (&usage.crate_.version)
+ }
+ }
+ }
+ }
+
+ pre .license-text { (&lic.text) }
+ }
+ }
+ },
+ )
+ }
+
+ fn layout(title: &str, content: Markup) -> Markup {
html! {
(maud::DOCTYPE)
html lang="en" {
head {
meta charset="UTF-8";
+ meta http-equiv="X-UA-Compatible" content="ie=edge";
meta name="viewport" content="width=device-width,initial-scale=1";
- link rel="stylesheet" href="assets/base.css";
- title { "Third Party Licenses — Earth PKG" }
+ title { (title) }
+ link rel="stylesheet" href="/base.css";
+
+ link rel="manifest" href="/manifest.webmanifest";
+ link rel="icon" href="/favicon.ico" sizes="32x32";
+ link rel="icon" href="/favicon.svg" type="image/svg+xml";
+ link rel="apple-touch-icon" href="/apple-touch-icon.png";
}
body {
- main {
- h1 { "Third Party Licenses" }
- p { "This page lists the licenses of the projects used in Zorian." }
-
- h2 { "Overview of licenses:" }
- ul {
- @for ov in &data.overview {
- li { a href=(format!("#{}", ov.id)) { (&ov.name) } " (" (ov.count) ")" }
- }
- }
-
- h2 { "All license text:" }
-
- @for (i, lic) in data.licenses.iter().enumerate() {
- div.license {
- @if lic.first_of_kind {
- h3 id=(&lic.id) { (&lic.name) }
- }
- h4 id=(format!("{}-{}", lic.id, i)) { (&lic.name) }
-
- h5 { "Used by:" }
- ul .license-used-by {
- @for usage in &lic.used_by {
- li {
- a href=(
- usage.crate_.repository.as_deref()
- .unwrap_or(&format!("https://crates.io/crates/{}", usage.crate_.name))
- ) {
- (&usage.crate_.name) " " (&usage.crate_.version)
- }
- }
- }
- }
-
- pre .license-text { (&lic.text) }
- }
- }
- }
+ (content)
}
}
}
@@ -374,26 +389,21 @@ impl WebController {
}
#[derive(Clone)]
-pub struct LastModifiedLayer {}
-impl<S> Layer<S> for LastModifiedLayer {
- type Service = LastModifiedService<S>;
- fn layer(&self, inner: S) -> Self::Service {
- LastModifiedService {
- inner,
+struct CacheLayer;
- // 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(),
- }
+impl<S> Layer<S> for CacheLayer {
+ type Service = CacheService<S>;
+ fn layer(&self, inner: S) -> Self::Service {
+ CacheService { inner }
}
}
#[derive(Clone)]
-pub struct LastModifiedService<S> {
+struct CacheService<S> {
inner: S,
- last_moidified: DateTime<Utc>,
}
-impl<S> Service<Request<Body>> for LastModifiedService<S>
+
+impl<S> Service<Request<Body>> for CacheService<S>
where
S: Service<Request<Body>, Response = Response<Body>> + Clone + Send + 'static,
S::Future: Send + 'static,
@@ -409,48 +419,50 @@ where
fn call(&mut self, req: Request<Body>) -> Self::Future {
let method = req.method().clone();
- let headers = req.headers().clone();
- let last_modified = self.last_moidified;
+ let if_none_match = req.headers().get(header::IF_NONE_MATCH).cloned();
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();
+ let resp = inner.call(req).await?;
+
+ if !resp.status().is_success() {
+ return Ok(resp);
+ }
+
+ let (parts, body) = resp.into_parts();
+ let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
+
+ let mut hasher = crc32fast::Hasher::new();
+ hasher.update(&bytes);
+ let etag = format!("\"{}\"", hex::encode(hasher.finalize().to_be_bytes()));
+ let etag_header = HeaderValue::from_str(&etag).unwrap();
+ let cache_control = parts
+ .headers
+ .get(header::CACHE_CONTROL)
+ .cloned()
+ .unwrap_or(HeaderValue::from_static("no-cache"));
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)
+ && if_none_match
+ .as_ref()
+ .is_some_and(|v| v.as_bytes() == etag.as_bytes())
{
- match Response::builder()
+ let mut resp = 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}")
- }
- }
+ .unwrap();
+ resp.headers_mut().insert(header::ETAG, etag_header);
+ resp.headers_mut()
+ .insert(header::CACHE_CONTROL, cache_control);
+ return Ok(resp);
}
- let mut resp = inner.call(req).await?;
- resp.headers_mut().insert(header::LAST_MODIFIED, lmh);
+ let mut resp = Response::from_parts(parts, Body::from(bytes));
+ resp.headers_mut().insert(header::ETAG, etag_header);
+ resp.headers_mut()
+ .entry(header::CACHE_CONTROL)
+ .or_insert(HeaderValue::from_static("no-cache"));
Ok(resp)
})
}
}
-
-/// Parse HTTP-date (IMF-fixdate per RFC 7231) or RFC 2822
-fn parse_http_date(value: &str) -> Option<DateTime<Utc>> {
- // IMF-fixdate: "Tue, 14 Jan 2026 12:34:56 GMT"
- DateTime::parse_from_rfc2822(value.trim())
- .map(|d| d.with_timezone(&Utc))
- .ok()
-}
diff --git a/crates/zorian/src/main.rs b/crates/zorian/src/main.rs
index 7b8ed42..c43c9d6 100644
--- a/crates/zorian/src/main.rs
+++ b/crates/zorian/src/main.rs
@@ -5,7 +5,6 @@ mod config;
mod proxy;
mod storage;
mod telemetry;
-mod utils;
mod controller_backend;
mod controller_web;
diff --git a/deny.toml b/deny.toml
index 0f7b2a5..563ab6a 100644
--- a/deny.toml
+++ b/deny.toml
@@ -22,9 +22,10 @@ allow = [
]
exceptions = [
# The project itself is distributed under the AGPL, but avoid it in dependencies
- { crate = "zorian", allow = ["AGPL-3.0-or-later"] },
- { crate = "repos", allow = ["AGPL-3.0-or-later"] },
{ crate = "xtask", allow = ["AGPL-3.0-or-later"] },
+ { crate = "base", allow = ["AGPL-3.0-or-later"] },
+ { crate = "repos", allow = ["AGPL-3.0-or-later"] },
+ { crate = "zorian", allow = ["AGPL-3.0-or-later"] },
]
unused-allowed-license = "allow"
diff --git a/pkg/logo.svg b/pkg/logo.svg
new file mode 100644
--- /dev/null
+++ b/pkg/logo.svg
@@ -0,0 +1,30 @@
+<!--
+SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
+SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg viewBox="-150 -150 800 800" xmlns="http://www.w3.org/2000/svg" fill="rgb(247, 164, 29)">
+ <rect x="0" y="100" width="100" height="100"></rect>
+ <rect x="0" y="200" width="100" height="100"></rect>
+ <rect x="0" y="300" width="100" height="100"></rect>
+ <rect x="0" y="400" width="100" height="100"></rect>
+ <rect x="100" y="0" width="100" height="100"></rect>
+ <rect x="100" y="100" width="100" height="100"></rect>
+ <rect x="100" y="300" width="100" height="100"></rect>
+ <rect x="100" y="400" width="100" height="100"></rect>
+ <rect x="200" y="0" width="100" height="100"></rect>
+ <rect x="200" y="100" width="100" height="100"></rect>
+ <rect x="200" y="200" width="100" height="100"></rect>
+ <rect x="200" y="300" width="100" height="100"></rect>
+ <rect x="200" y="400" width="100" height="100"></rect>
+ <rect x="400" y="100" width="100" height="100"></rect>
+ <rect x="400" y="200" width="100" height="100"></rect>
+ <rect x="400" y="300" width="100" height="100"></rect>
+ <rect x="400" y="400" width="100" height="100"></rect>
+ <rect x="300" y="0" width="100" height="100"></rect>
+ <rect x="300" y="100" width="100" height="100"></rect>
+ <rect x="300" y="300" width="100" height="100"></rect>
+ <rect x="300" y="400" width="100" height="100"></rect>
+</svg>
diff --git a/xtask/build.rs b/xtask/build.rs
new file mode 100644
--- /dev/null
+++ b/xtask/build.rs
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+fn main() {
+ println!(
+ "cargo:rustc-env=TARGET={}",
+ std::env::var("TARGET").unwrap()
+ );
+}
diff --git a/xtask/src/licenses.rs b/xtask/src/licenses.rs
index 162a090..49736d8 100644
--- a/xtask/src/licenses.rs
+++ b/xtask/src/licenses.rs
@@ -135,7 +135,7 @@ fn get_all_crates(cargo_toml: &Utf8Path) -> Result<Krates, krates::Error> {
builder.ignore_kind(krates::DepKind::Build, krates::Scope::NonWorkspace);
}
- builder.include_targets(std::iter::empty::<(&str, Vec<String>)>());
+ builder.include_targets(std::iter::once((env!("TARGET"), vec![])));
let graph = builder.build(mdc, |filtered: cm::Package| {
tracing::debug!("filtered {} {}", filtered.name, filtered.version);