diff options
| author | Nikolay Govorov <me@govorov.online> | 2026-02-03 11:45:32 +0000 |
|---|---|---|
| committer | Nikolay Govorov <me@govorov.online> | 2026-02-03 11:45:32 +0000 |
| commit | fb11f922a50e9189f709e31e767199b8ac4f4ca5 (patch) | |
| tree | 74507578667b26eb0967d117c3fcb1fec72aba90 | |
| parent | 9e455c20a929ddae9e142ee0eb1554df1faf30b1 (diff) | |
| download | tar tar.gz tar.bz2 tar.lz tar.xz tar.zst zip | |
Optimizes loading of minisig files for Zig
Diffstat
| -rw-r--r-- | src/assets/base.css | 8 | +5 −3 |
| -rw-r--r-- | src/backends/go.rs | 8 | +6 −2 |
| -rw-r--r-- | src/backends/mod.rs | 5 | +3 −2 |
| -rw-r--r-- | src/backends/zig.rs | 223 | +198 −25 |
| -rw-r--r-- | src/controller_backend.rs | 31 | +16 −15 |
| -rw-r--r-- | src/controller_web.rs | 9 | +4 −5 |
6 files changed, 232 insertions, 52 deletions
diff --git a/src/assets/base.css b/src/assets/base.css index d102d49..ebdd940 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -39,9 +39,11 @@ body { padding: 1.5em 2em; } -pre, code { - font-size: .75rem; - font-weight: 600; +code { + margin: 0; + padding: .2em .4em; + white-space: break-spaces; + background-color: #818b981f; } h1, h2, h3, h4, h5, h6 { diff --git a/src/backends/go.rs b/src/backends/go.rs index 41d7a42..a941387 100644 --- a/src/backends/go.rs +++ b/src/backends/go.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use std::time::Duration; +use mime_guess::mime; use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; @@ -458,7 +459,7 @@ impl Backend for GoBackend { Ok(Some(hash)) => { return Ok(ResolvedFile::Content { data: hash.into(), - mime: "text/plain", + mime: mime::TEXT_PLAIN, }); } Ok(None) => return Err(ResolveError::NotFound), @@ -489,7 +490,10 @@ impl Backend for GoBackend { let url = file .upstream_url(&self.config.upstream, &self.source) .map_err(|_| ResolveError::Internal)?; - Ok(ResolvedFile::Upstream(url)) + Ok(ResolvedFile::Upstream { + url, + mime: mime::APPLICATION_OCTET_STREAM, + }) } async fn migrate(&self) -> Result<(), IndexError> { diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 0307314..1bda2d9 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -8,6 +8,7 @@ use std::time::Duration; use async_trait::async_trait; use bytes::Bytes; +use mime_guess::mime; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Sqlite}; use thiserror::Error; @@ -59,11 +60,11 @@ pub enum ResolveError { /// Result of resolving a file request. pub enum ResolvedFile { /// Proxy request to upstream URL - Upstream(Url), + Upstream { mime: mime::Mime, url: Url }, /// Return content directly Content { + mime: mime::Mime, data: bytes::Bytes, - mime: &'static str, }, } diff --git a/src/backends/zig.rs b/src/backends/zig.rs index 8ffcde3..903f4b1 100644 --- a/src/backends/zig.rs +++ b/src/backends/zig.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use mime_guess::mime; use semver::Version as SemVersion; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -143,12 +144,16 @@ impl<'a> ZigFile<'a> { if payload == "bootstrap" { (None, None, FileKind::Bootstrap) } else { - // Version 0.14.0 is the last one to use the OS-ARCH format in names; newer versions use ARCH-OS. - let min_version = SemVersion::new(0, 14, 0); - let (os, arch) = if version > min_version { - (payload, it.next().ok_or(ParseError)?) - } else { + // Filename format changed over time: + // - <= 0.2.0: used zig-win64 for windows and zig-linux-x86_64 for linux ¯\_(ツ)_/¯ + // - 0.2.0 to 0.14.0: zig-OS-ARCH-VERSION (e.g. zig-linux-x86_64-0.13.0) + // - > 0.14.0: zig-ARCH-OS-VERSION (e.g. zig-x86_64-linux-0.15.0) + let (os, arch) = if version <= SemVersion::new(0, 2, 0) && payload == "win64" { + ("windows", "x86_64") + } else if version <= SemVersion::new(0, 14, 0) { (it.next().ok_or(ParseError)?, payload) + } else { + (payload, it.next().ok_or(ParseError)?) }; (Some(os), Some(arch), FileKind::Archive) } @@ -264,29 +269,48 @@ impl Backend for ZigBackend { async fn resolve_file(&self, filename: &str) -> Result<ResolvedFile, ResolveError> { let file = ZigFile::parse(filename).map_err(|_| ResolveError::NotFound)?; - - // For stable builds, check that file exists in index - if !file.development { - let exists: Result<Option<i32>, _> = - sqlx::query_scalar("SELECT 1 FROM zig_files WHERE filename = ?1") - .bind(filename) - .fetch_optional(self.delegate.db()) - .await; - - match exists { - Ok(None) => return Err(ResolveError::NotFound), - Err(e) => { - tracing::error!(filename, "failed to check file existence: {e}"); - return Err(ResolveError::Internal); - } - Ok(Some(_)) => {} - } - } - + let mime = if file.minisig { + mime::TEXT_PLAIN + } else { + mime::APPLICATION_OCTET_STREAM + }; let url = file .upstream_url(&self.config.upstream, &self.source) .map_err(|_| ResolveError::Internal)?; - Ok(ResolvedFile::Upstream(url)) + + // For stable builds, check index + let base_filename = filename.strip_suffix(".minisig").unwrap_or(filename); + let row: Option<Option<String>> = + sqlx::query_scalar("SELECT minisig FROM zig_files WHERE filename = ?1") + .bind(base_filename) + .fetch_optional(self.delegate.db()) + .await + .map_err(|e| { + tracing::error!(filename, "failed to query file: {e}"); + ResolveError::Internal + })?; + + // File not in index + if row.is_none() { + // Dev builds go directly to upstream without index check + return if file.development { + Ok(ResolvedFile::Upstream { url, mime }) + } else { + Err(ResolveError::NotFound) + }; + } + + // Return cached minisig if available + if file.minisig + && let Some(Some(data)) = row + { + return Ok(ResolvedFile::Content { + data: data.into(), + mime: mime::TEXT_PLAIN, + }); + } + + Ok(ResolvedFile::Upstream { url, mime }) } async fn migrate(&self) -> Result<(), IndexError> { @@ -311,6 +335,7 @@ impl Backend for ZigBackend { filename TEXT NOT NULL, shasum TEXT NOT NULL, size INTEGER NOT NULL, + minisig TEXT, PRIMARY KEY (version, target), FOREIGN KEY (version) REFERENCES zig_versions(version) ) STRICT", @@ -319,6 +344,16 @@ impl Backend for ZigBackend { .await .map_err(|e| IndexError::Database(e.to_string()))?; + // Migration: add minisig column to existing tables + match sqlx::query("ALTER TABLE zig_files ADD COLUMN minisig TEXT") + .execute(self.delegate.db()) + .await + { + Ok(_) => {} + Err(sqlx::Error::Database(e)) if e.message().contains("duplicate column") => {} + Err(e) => return Err(IndexError::Database(e.to_string())), + } + Ok(()) } @@ -339,6 +374,8 @@ impl Backend for ZigBackend { } } + self.fetch_minisigs().await?; + Ok(()) } @@ -492,4 +529,140 @@ impl ZigBackend { Ok(()) } + + async fn fetch_minisigs(&self) -> Result<(), IndexError> { + let files: Vec<(String,)> = + sqlx::query_as("SELECT filename FROM zig_files WHERE minisig IS NULL") + .fetch_all(self.delegate.db()) + .await + .map_err(|e| IndexError::Database(e.to_string()))?; + + for (filename,) in files { + if let Err(e) = self.fetch_minisig(&filename).await { + tracing::debug!(filename, "failed to fetch minisig: {e}"); + } + } + + Ok(()) + } + + async fn fetch_minisig(&self, filename: &str) -> Result<(), IndexError> { + let minisig_filename = format!("{}.minisig", filename); + let file = ZigFile::parse(&minisig_filename) + .map_err(|_| IndexError::Parse(format!("invalid filename: {}", filename)))?; + + let url = file + .upstream_url(&self.config.upstream, &self.source) + .map_err(|_| IndexError::Parse("cannot build URL".into()))?; + + let bytes = self.delegate.http_get(&url).await?; + let minisig = + String::from_utf8(bytes.to_vec()).map_err(|e| IndexError::Parse(e.to_string()))?; + + sqlx::query("UPDATE zig_files SET minisig = ?1 WHERE filename = ?2") + .bind(&minisig) + .bind(filename) + .execute(self.delegate.db()) + .await + .map_err(|e| IndexError::Database(e.to_string()))?; + + tracing::debug!(filename, "cached minisig"); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_old_combined_platform() { + // <= 0.2.0: zig-PLATFORM-VERSION (only Windows used this format) + let file = ZigFile::parse("zig-win64-0.1.1.zip").unwrap(); + assert_eq!(file.os, Some("windows")); + assert_eq!(file.arch, Some("x86_64")); + assert_eq!(file.version, SemVersion::new(0, 1, 1)); + assert_eq!(file.archive, Archive::Zip); + assert_eq!(file.kind, FileKind::Archive); + + let file = ZigFile::parse("zig-win64-0.2.0.zip").unwrap(); + assert_eq!(file.os, Some("windows")); + assert_eq!(file.arch, Some("x86_64")); + assert_eq!(file.version, SemVersion::new(0, 2, 0)); + } + + #[test] + fn parse_middle_os_arch_format() { + // 0.2.0 to 0.14.0: zig-OS-ARCH-VERSION + let file = ZigFile::parse("zig-linux-x86_64-0.13.0.tar.xz").unwrap(); + assert_eq!(file.os, Some("linux")); + assert_eq!(file.arch, Some("x86_64")); + assert_eq!(file.version, SemVersion::new(0, 13, 0)); + assert_eq!(file.archive, Archive::TarXz); + + let file = ZigFile::parse("zig-windows-x86_64-0.10.0.zip").unwrap(); + assert_eq!(file.os, Some("windows")); + assert_eq!(file.arch, Some("x86_64")); + } + + #[test] + fn parse_new_arch_os_format() { + // > 0.14.0: zig-ARCH-OS-VERSION + let file = ZigFile::parse("zig-x86_64-linux-0.15.0.tar.xz").unwrap(); + assert_eq!(file.os, Some("linux")); + assert_eq!(file.arch, Some("x86_64")); + assert_eq!(file.version, SemVersion::new(0, 15, 0)); + + let file = ZigFile::parse("zig-aarch64-macos-0.15.0.tar.xz").unwrap(); + assert_eq!(file.os, Some("macos")); + assert_eq!(file.arch, Some("aarch64")); + } + + #[test] + fn parse_dev_version() { + let file = ZigFile::parse("zig-x86_64-linux-0.14.0-dev.123+abc123.tar.xz").unwrap(); + assert!(file.development); + assert_eq!(file.version.major, 0); + assert_eq!(file.version.minor, 14); + assert_eq!(file.version.patch, 0); + } + + #[test] + fn parse_source_tarball() { + let file = ZigFile::parse("zig-0.13.0.tar.xz").unwrap(); + assert_eq!(file.os, None); + assert_eq!(file.arch, None); + assert_eq!(file.kind, FileKind::Source); + } + + #[test] + fn parse_bootstrap() { + let file = ZigFile::parse("zig-bootstrap-0.13.0.tar.xz").unwrap(); + assert_eq!(file.kind, FileKind::Bootstrap); + } + + #[test] + fn parse_minisig() { + let file = ZigFile::parse("zig-win64-0.1.1.zip.minisig").unwrap(); + assert!(file.minisig); + assert_eq!(file.os, Some("windows")); + assert_eq!(file.arch, Some("x86_64")); + + let file = ZigFile::parse("zig-x86_64-linux-0.15.0.tar.xz.minisig").unwrap(); + assert!(file.minisig); + assert_eq!(file.os, Some("linux")); + } + + #[test] + fn parse_boundary_version() { + // 0.14.0 should use OS-ARCH format + let file = ZigFile::parse("zig-linux-x86_64-0.14.0.tar.xz").unwrap(); + assert_eq!(file.os, Some("linux")); + assert_eq!(file.arch, Some("x86_64")); + + // 0.14.1 should use ARCH-OS format + let file = ZigFile::parse("zig-x86_64-linux-0.14.1.tar.xz").unwrap(); + assert_eq!(file.os, Some("linux")); + assert_eq!(file.arch, Some("x86_64")); + } } diff --git a/src/controller_backend.rs b/src/controller_backend.rs index dfbfeb1..23ebbbd 100644 --- a/src/controller_backend.rs +++ b/src/controller_backend.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use axum::{Router, body, extract, http, response, routing}; +use mime_guess::mime; use tracing::error; use crate::backends::{Backend, ResolveError, ResolvedFile}; @@ -40,11 +41,11 @@ impl<B: Backend> BackendController<B> { extract::State(controller): extract::State<Arc<Self>>, extract::Path(filename): extract::Path<String>, ) -> Result<response::Response, http::StatusCode> { - let url = match controller.backend.resolve_file(&filename).await { + let (url, mime) = match controller.backend.resolve_file(&filename).await { Ok(ResolvedFile::Content { data, mime }) => { - return Ok(Self::build_response_with_mime(data, mime)); + return Ok(Self::build_response(http::StatusCode::OK, data, mime)); } - Ok(ResolvedFile::Upstream(url)) => url, + Ok(ResolvedFile::Upstream { url, mime }) => (url, mime), Err(ResolveError::NotFound) => { error!(backend = B::ID, filename, "file not found"); return Err(http::StatusCode::NOT_FOUND); @@ -60,6 +61,7 @@ impl<B: Backend> BackendController<B> { return Ok(Self::build_response( http::StatusCode::OK, entry.file_bytes.0, + mime, )); } Ok(None) => {} @@ -88,22 +90,21 @@ impl<B: Backend> BackendController<B> { } } - Ok(Self::build_response(http::StatusCode::OK, entry.bytes)) + Ok(Self::build_response( + http::StatusCode::OK, + entry.bytes, + mime, + )) } - fn build_response(status: http::StatusCode, bytes: bytes::Bytes) -> response::Response { + fn build_response( + status: http::StatusCode, + bytes: bytes::Bytes, + mime: mime::Mime, + ) -> 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() - } - - fn build_response_with_mime(bytes: bytes::Bytes, mime: &'static str) -> response::Response { - response::Response::builder() - .status(http::StatusCode::OK) - .header(http::header::CONTENT_TYPE, mime) + .header(http::header::CONTENT_TYPE, mime.as_ref()) .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 index 877e243..1c33296 100644 --- a/src/controller_web.rs +++ b/src/controller_web.rs @@ -181,15 +181,15 @@ impl WebController { } td { @if let Some(ref src) = v.src { - a href=(format!("/zig/{}", src.filename)) { code { "src" } } + a href=(format!("/zig/{}", src.filename)) { "src" } " " } @if let Some(ref bootstrap) = v.bootstrap { - a href=(format!("/zig/{}", bootstrap.filename)) { code { "bootstrap" } } + a href=(format!("/zig/{}", bootstrap.filename)) { "bootstrap" } " " } @for (target, tarball) in v.targets.iter() { - a href=(format!("/zig/{}", tarball.filename)) { code { (target) } } + a href=(format!("/zig/{}", tarball.filename)) { (target) } " " } } @@ -200,7 +200,6 @@ 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" } "." @@ -244,7 +243,7 @@ impl WebController { td { @if v.stable { "✓" } @else { "" } } td { @for file in &v.files { - a href=(format!("/go/{}", file.filename)) { code { (file.filename) } } + a href=(format!("/go/{}", file.filename)) { (file.filename) } " " } } |
