aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--src/assets/base.css8+5 −3
-rw-r--r--src/backends/go.rs8+6 −2
-rw-r--r--src/backends/mod.rs5+3 −2
-rw-r--r--src/backends/zig.rs223+198 −25
-rw-r--r--src/controller_backend.rs31+16 −15
-rw-r--r--src/controller_web.rs9+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) }
" "
}
}