mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 12:40:31 +00:00
feat: add Go module proxy (GOPROXY protocol) (#59)
* feat: add Go module proxy (GOPROXY protocol) (#47) Implements caching proxy for Go modules with 5 standard endpoints: - GET /go/{module}/@v/list — list versions - GET /go/{module}/@v/{version}.info — version metadata - GET /go/{module}/@v/{version}.mod — go.mod file - GET /go/{module}/@v/{version}.zip — module zip - GET /go/{module}/@latest — latest version info Features: - Module path encoding/decoding per Go spec (!x → X) - Immutable caching (.info/.mod/.zip never overwritten) - Mutable endpoints (@v/list, @latest) refreshed from upstream - Configurable upstream (default: proxy.golang.org) - Separate timeout for .zip downloads (default: 120s) - Size limit for zips (default: 100MB) - Path traversal protection - Dashboard integration (stats, mount points, index) - 25 unit tests (encoding, path splitting, safety, content-type) Closes #47 * style: cargo fmt * feat(ui): add Go pages, compact cards, fix icons - Go in sidebar + list/detail pages with go get command - Dashboard: fix fallback icon (was Docker whale for Go) - Compact registry cards: lg:grid-cols-6, all 6 in one row - Cargo icon: crate boxes instead of truck - Go icon: stylized Go text (sidebar + dashboard) * fix(go): URL-decode path + send encoded paths to upstream Go client sends %21 for ! in module paths. Axum wildcard does not auto-decode, so we percent-decode manually. Upstream proxy.golang.org expects encoded paths (with !), not decoded uppercase. Tested: full Pusk build (22 modules, 135MB cached) including SherClockHolmes/webpush-go with triple uppercase encoding. * style: cargo fmt
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1321,6 +1321,7 @@ dependencies = [
|
|||||||
"indicatif",
|
"indicatif",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"percent-encoding",
|
||||||
"prometheus",
|
"prometheus",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ parking_lot = "0.12"
|
|||||||
zeroize = { version = "1.8", features = ["derive"] }
|
zeroize = { version = "1.8", features = ["derive"] }
|
||||||
argon2 = { version = "0.5", features = ["std", "rand"] }
|
argon2 = { version = "0.5", features = ["std", "rand"] }
|
||||||
tower-http = { version = "0.6", features = ["set-header"] }
|
tower-http = { version = "0.6", features = ["set-header"] }
|
||||||
|
percent-encoding = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub docker: DockerConfig,
|
pub docker: DockerConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub go: GoConfig,
|
||||||
|
#[serde(default)]
|
||||||
pub raw: RawConfig,
|
pub raw: RawConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auth: AuthConfig,
|
pub auth: AuthConfig,
|
||||||
@@ -127,6 +129,48 @@ pub struct PypiConfig {
|
|||||||
pub proxy_timeout: u64,
|
pub proxy_timeout: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Go module proxy configuration (GOPROXY protocol)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GoConfig {
|
||||||
|
/// Upstream Go module proxy URL (default: https://proxy.golang.org)
|
||||||
|
#[serde(default = "default_go_proxy")]
|
||||||
|
pub proxy: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub proxy_auth: Option<String>, // "user:pass" for basic auth
|
||||||
|
#[serde(default = "default_timeout")]
|
||||||
|
pub proxy_timeout: u64,
|
||||||
|
/// Separate timeout for .zip downloads (default: 120s, zips can be large)
|
||||||
|
#[serde(default = "default_go_zip_timeout")]
|
||||||
|
pub proxy_timeout_zip: u64,
|
||||||
|
/// Maximum module zip size in bytes (default: 100MB)
|
||||||
|
#[serde(default = "default_go_max_zip_size")]
|
||||||
|
pub max_zip_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_go_proxy() -> Option<String> {
|
||||||
|
Some("https://proxy.golang.org".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_go_zip_timeout() -> u64 {
|
||||||
|
120
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_go_max_zip_size() -> u64 {
|
||||||
|
104_857_600 // 100MB
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GoConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
proxy: default_go_proxy(),
|
||||||
|
proxy_auth: None,
|
||||||
|
proxy_timeout: 30,
|
||||||
|
proxy_timeout_zip: 120,
|
||||||
|
max_zip_size: 104_857_600,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Docker registry configuration with upstream proxy support
|
/// Docker registry configuration with upstream proxy support
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DockerConfig {
|
pub struct DockerConfig {
|
||||||
@@ -387,6 +431,10 @@ impl Config {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Go
|
||||||
|
if self.go.proxy_auth.is_some() && std::env::var("NORA_GO_PROXY_AUTH").is_err() {
|
||||||
|
tracing::warn!("Go proxy credentials in config.toml are plaintext — consider NORA_GO_PROXY_AUTH env var");
|
||||||
|
}
|
||||||
// npm
|
// npm
|
||||||
if self.npm.proxy_auth.is_some() && std::env::var("NORA_NPM_PROXY_AUTH").is_err() {
|
if self.npm.proxy_auth.is_some() && std::env::var("NORA_NPM_PROXY_AUTH").is_err() {
|
||||||
tracing::warn!("npm proxy credentials in config.toml are plaintext — consider NORA_NPM_PROXY_AUTH env var");
|
tracing::warn!("npm proxy credentials in config.toml are plaintext — consider NORA_NPM_PROXY_AUTH env var");
|
||||||
@@ -629,6 +677,7 @@ impl Default for Config {
|
|||||||
maven: MavenConfig::default(),
|
maven: MavenConfig::default(),
|
||||||
npm: NpmConfig::default(),
|
npm: NpmConfig::default(),
|
||||||
pypi: PypiConfig::default(),
|
pypi: PypiConfig::default(),
|
||||||
|
go: GoConfig::default(),
|
||||||
docker: DockerConfig::default(),
|
docker: DockerConfig::default(),
|
||||||
raw: RawConfig::default(),
|
raw: RawConfig::default(),
|
||||||
auth: AuthConfig::default(),
|
auth: AuthConfig::default(),
|
||||||
|
|||||||
@@ -326,7 +326,8 @@ async fn run_server(config: Config, storage: Storage) {
|
|||||||
.merge(registry::npm_routes())
|
.merge(registry::npm_routes())
|
||||||
.merge(registry::cargo_routes())
|
.merge(registry::cargo_routes())
|
||||||
.merge(registry::pypi_routes())
|
.merge(registry::pypi_routes())
|
||||||
.merge(registry::raw_routes());
|
.merge(registry::raw_routes())
|
||||||
|
.merge(registry::go_routes());
|
||||||
|
|
||||||
// Routes WITHOUT rate limiting (health, metrics, UI)
|
// Routes WITHOUT rate limiting (health, metrics, UI)
|
||||||
let public_routes = Router::new()
|
let public_routes = Router::new()
|
||||||
|
|||||||
522
nora-registry/src/registry/go.rs
Normal file
522
nora-registry/src/registry/go.rs
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//! Go module proxy (GOPROXY protocol).
|
||||||
|
//!
|
||||||
|
//! Implements the 5 required endpoints:
|
||||||
|
//! GET /go/{module}/@v/list — list known versions
|
||||||
|
//! GET /go/{module}/@v/{ver}.info — version metadata (JSON)
|
||||||
|
//! GET /go/{module}/@v/{ver}.mod — go.mod file
|
||||||
|
//! GET /go/{module}/@v/{ver}.zip — module zip archive
|
||||||
|
//! GET /go/{module}/@latest — latest version info
|
||||||
|
|
||||||
|
use crate::activity_log::{ActionType, ActivityEntry};
|
||||||
|
use crate::audit::AuditEntry;
|
||||||
|
use crate::registry::{proxy_fetch, proxy_fetch_text, ProxyError};
|
||||||
|
use crate::AppState;
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::{header, HeaderValue, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use percent_encoding::percent_decode;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn routes() -> Router<Arc<AppState>> {
|
||||||
|
Router::new().route("/go/{*path}", get(handle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main handler — parses the wildcard path and dispatches to the right logic.
|
||||||
|
async fn handle(State(state): State<Arc<AppState>>, Path(path): Path<String>) -> Response {
|
||||||
|
// URL-decode the path: Go client sends %21 for !, Axum wildcard may not decode it
|
||||||
|
let path = percent_decode(path.as_bytes())
|
||||||
|
.decode_utf8()
|
||||||
|
.map(|s| s.into_owned())
|
||||||
|
.unwrap_or(path);
|
||||||
|
|
||||||
|
tracing::debug!(path = %path, "Go proxy request");
|
||||||
|
|
||||||
|
// Validate path: no traversal, no null bytes
|
||||||
|
if !is_safe_path(&path) {
|
||||||
|
tracing::debug!(path = %path, "Go proxy: unsafe path");
|
||||||
|
return StatusCode::BAD_REQUEST.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split: "github.com/!azure/sdk/@v/v1.0.0.info" → module + file
|
||||||
|
let (module_encoded, file) = match split_go_path(&path) {
|
||||||
|
Some(parts) => parts,
|
||||||
|
None => {
|
||||||
|
tracing::debug!(path = %path, "Go proxy: cannot split path");
|
||||||
|
return StatusCode::NOT_FOUND.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let storage_key = format!("go/{}", path);
|
||||||
|
let content_type = content_type_for(&file);
|
||||||
|
|
||||||
|
// Mutable endpoints: @v/list and @latest can be refreshed from upstream
|
||||||
|
let is_mutable = file == "@v/list" || file == "@latest";
|
||||||
|
// Immutable: .info, .mod, .zip — once cached, never overwrite
|
||||||
|
let is_immutable = !is_mutable;
|
||||||
|
|
||||||
|
// 1. Try local cache (for immutable files, this is authoritative)
|
||||||
|
if let Ok(data) = state.storage.get(&storage_key).await {
|
||||||
|
state.metrics.record_download("go");
|
||||||
|
state.metrics.record_cache_hit();
|
||||||
|
state.activity.push(ActivityEntry::new(
|
||||||
|
ActionType::CacheHit,
|
||||||
|
format_artifact(&module_encoded, &file),
|
||||||
|
"go",
|
||||||
|
"CACHE",
|
||||||
|
));
|
||||||
|
return with_content_type(data.to_vec(), content_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try upstream proxy
|
||||||
|
let proxy_url = match &state.config.go.proxy {
|
||||||
|
Some(url) => url.clone(),
|
||||||
|
None => return StatusCode::NOT_FOUND.into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate module path encoding (but keep encoded for upstream — proxy.golang.org expects ! encoding)
|
||||||
|
if decode_module_path(&module_encoded).is_err() {
|
||||||
|
return StatusCode::BAD_REQUEST.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let upstream_url = format!(
|
||||||
|
"{}/{}",
|
||||||
|
proxy_url.trim_end_matches('/'),
|
||||||
|
format_upstream_path(&module_encoded, &file)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use longer timeout for .zip files
|
||||||
|
let timeout = if file.ends_with(".zip") {
|
||||||
|
state.config.go.proxy_timeout_zip
|
||||||
|
} else {
|
||||||
|
state.config.go.proxy_timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch: binary for .zip, text for everything else
|
||||||
|
let data = if file.ends_with(".zip") {
|
||||||
|
proxy_fetch(
|
||||||
|
&state.http_client,
|
||||||
|
&upstream_url,
|
||||||
|
timeout,
|
||||||
|
state.config.go.proxy_auth.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
proxy_fetch_text(
|
||||||
|
&state.http_client,
|
||||||
|
&upstream_url,
|
||||||
|
timeout,
|
||||||
|
state.config.go.proxy_auth.as_deref(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|s| s.into_bytes())
|
||||||
|
};
|
||||||
|
|
||||||
|
match data {
|
||||||
|
Ok(bytes) => {
|
||||||
|
// Enforce size limit for .zip
|
||||||
|
if file.ends_with(".zip") && bytes.len() as u64 > state.config.go.max_zip_size {
|
||||||
|
tracing::warn!(
|
||||||
|
module = module_encoded,
|
||||||
|
size = bytes.len(),
|
||||||
|
limit = state.config.go.max_zip_size,
|
||||||
|
"Go module zip exceeds size limit"
|
||||||
|
);
|
||||||
|
return StatusCode::PAYLOAD_TOO_LARGE.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.metrics.record_download("go");
|
||||||
|
state.metrics.record_cache_miss();
|
||||||
|
state.activity.push(ActivityEntry::new(
|
||||||
|
ActionType::ProxyFetch,
|
||||||
|
format_artifact(&module_encoded, &file),
|
||||||
|
"go",
|
||||||
|
"PROXY",
|
||||||
|
));
|
||||||
|
state
|
||||||
|
.audit
|
||||||
|
.log(AuditEntry::new("proxy_fetch", "api", "", "go", ""));
|
||||||
|
|
||||||
|
// Background cache: immutable = put_if_absent, mutable = always overwrite
|
||||||
|
let storage = state.storage.clone();
|
||||||
|
let key = storage_key.clone();
|
||||||
|
let data_clone = bytes.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if is_immutable {
|
||||||
|
// Only write if not already cached (immutability guarantee)
|
||||||
|
if storage.stat(&key).await.is_none() {
|
||||||
|
let _ = storage.put(&key, &data_clone).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = storage.put(&key, &data_clone).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
state.repo_index.invalidate("go");
|
||||||
|
with_content_type(bytes, content_type)
|
||||||
|
}
|
||||||
|
Err(ProxyError::NotFound) => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::debug!(
|
||||||
|
module = module_encoded,
|
||||||
|
file = file,
|
||||||
|
error = ?e,
|
||||||
|
"Go upstream proxy error"
|
||||||
|
);
|
||||||
|
StatusCode::BAD_GATEWAY.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Module path encoding/decoding
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Decode Go module path: `!x` → `X`
|
||||||
|
///
|
||||||
|
/// Go module proxy spec requires uppercase letters to be encoded as `!`
|
||||||
|
/// followed by the lowercase letter. Raw uppercase in encoded path is invalid.
|
||||||
|
fn decode_module_path(encoded: &str) -> Result<String, ()> {
|
||||||
|
let mut result = String::with_capacity(encoded.len());
|
||||||
|
let mut chars = encoded.chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '!' {
|
||||||
|
match chars.next() {
|
||||||
|
Some(next) if next.is_ascii_lowercase() => {
|
||||||
|
result.push(next.to_ascii_uppercase());
|
||||||
|
}
|
||||||
|
_ => return Err(()),
|
||||||
|
}
|
||||||
|
} else if c.is_ascii_uppercase() {
|
||||||
|
// Raw uppercase in encoded path is invalid per spec
|
||||||
|
return Err(());
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Go module path: `X` → `!x`
|
||||||
|
#[cfg(test)]
|
||||||
|
fn encode_module_path(path: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(path.len() + 8);
|
||||||
|
for c in path.chars() {
|
||||||
|
if c.is_ascii_uppercase() {
|
||||||
|
result.push('!');
|
||||||
|
result.push(c.to_ascii_lowercase());
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Path parsing helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Split Go path into (encoded_module, file).
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// "github.com/user/repo/@v/v1.0.0.info" → ("github.com/user/repo", "@v/v1.0.0.info")
|
||||||
|
/// "github.com/user/repo/v2/@v/list" → ("github.com/user/repo/v2", "@v/list")
|
||||||
|
/// "github.com/user/repo/@latest" → ("github.com/user/repo", "@latest")
|
||||||
|
fn split_go_path(path: &str) -> Option<(String, String)> {
|
||||||
|
// Try @latest first (it's simpler)
|
||||||
|
if let Some(pos) = path.rfind("/@latest") {
|
||||||
|
let module = &path[..pos];
|
||||||
|
if !module.is_empty() {
|
||||||
|
return Some((module.to_string(), "@latest".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try @v/ — find the last occurrence (handles /v2/@v/ correctly)
|
||||||
|
if let Some(pos) = path.rfind("/@v/") {
|
||||||
|
let module = &path[..pos];
|
||||||
|
let file = &path[pos + 1..]; // "@v/..."
|
||||||
|
if !module.is_empty() && !file.is_empty() {
|
||||||
|
return Some((module.to_string(), file.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Path validation: no traversal attacks
|
||||||
|
fn is_safe_path(path: &str) -> bool {
|
||||||
|
!path.contains("..")
|
||||||
|
&& !path.starts_with('/')
|
||||||
|
&& !path.contains("//")
|
||||||
|
&& !path.contains('\0')
|
||||||
|
&& !path.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content-Type for Go proxy responses
|
||||||
|
fn content_type_for(file: &str) -> &'static str {
|
||||||
|
if file.ends_with(".info") || file == "@latest" {
|
||||||
|
"application/json"
|
||||||
|
} else if file.ends_with(".zip") {
|
||||||
|
"application/zip"
|
||||||
|
} else {
|
||||||
|
// .mod, @v/list
|
||||||
|
"text/plain; charset=utf-8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build upstream URL path (uses decoded module path)
|
||||||
|
fn format_upstream_path(module_decoded: &str, file: &str) -> String {
|
||||||
|
format!("{}/{}", module_decoded, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human-readable artifact name for activity log
|
||||||
|
fn format_artifact(module: &str, file: &str) -> String {
|
||||||
|
if file == "@v/list" || file == "@latest" {
|
||||||
|
format!("{} {}", module, file)
|
||||||
|
} else if let Some(version_file) = file.strip_prefix("@v/") {
|
||||||
|
// "v1.0.0.info" → "module@v1.0.0"
|
||||||
|
let version = version_file
|
||||||
|
.rsplit_once('.')
|
||||||
|
.map(|(v, _ext)| v)
|
||||||
|
.unwrap_or(version_file);
|
||||||
|
format!("{}@{}", module, version)
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", module, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build response with Content-Type header
|
||||||
|
fn with_content_type(data: Vec<u8>, content_type: &'static str) -> Response {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
[(header::CONTENT_TYPE, HeaderValue::from_static(content_type))],
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// ── Encoding/decoding ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decode_azure() {
|
||||||
|
assert_eq!(
|
||||||
|
decode_module_path("github.com/!azure/sdk").unwrap(),
|
||||||
|
"github.com/Azure/sdk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decode_multiple_uppercase() {
|
||||||
|
assert_eq!(
|
||||||
|
decode_module_path("!google!cloud!platform/foo").unwrap(),
|
||||||
|
"GoogleCloudPlatform/foo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decode_no_uppercase() {
|
||||||
|
assert_eq!(
|
||||||
|
decode_module_path("github.com/user/repo").unwrap(),
|
||||||
|
"github.com/user/repo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decode_invalid_bang_at_end() {
|
||||||
|
assert!(decode_module_path("foo!").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decode_invalid_bang_followed_by_uppercase() {
|
||||||
|
assert!(decode_module_path("foo!A").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decode_raw_uppercase_is_invalid() {
|
||||||
|
assert!(decode_module_path("github.com/Azure/sdk").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_roundtrip() {
|
||||||
|
let original = "github.com/Azure/azure-sdk-for-go";
|
||||||
|
let encoded = encode_module_path(original);
|
||||||
|
assert_eq!(encoded, "github.com/!azure/azure-sdk-for-go");
|
||||||
|
assert_eq!(decode_module_path(&encoded).unwrap(), original);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_no_change() {
|
||||||
|
assert_eq!(
|
||||||
|
encode_module_path("github.com/user/repo"),
|
||||||
|
"github.com/user/repo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Path splitting ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_version_info() {
|
||||||
|
let (module, file) = split_go_path("github.com/user/repo/@v/v1.0.0.info").unwrap();
|
||||||
|
assert_eq!(module, "github.com/user/repo");
|
||||||
|
assert_eq!(file, "@v/v1.0.0.info");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_version_list() {
|
||||||
|
let (module, file) = split_go_path("github.com/user/repo/@v/list").unwrap();
|
||||||
|
assert_eq!(module, "github.com/user/repo");
|
||||||
|
assert_eq!(file, "@v/list");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_latest() {
|
||||||
|
let (module, file) = split_go_path("github.com/user/repo/@latest").unwrap();
|
||||||
|
assert_eq!(module, "github.com/user/repo");
|
||||||
|
assert_eq!(file, "@latest");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_major_version_suffix() {
|
||||||
|
let (module, file) = split_go_path("github.com/user/repo/v2/@v/list").unwrap();
|
||||||
|
assert_eq!(module, "github.com/user/repo/v2");
|
||||||
|
assert_eq!(file, "@v/list");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_incompatible_version() {
|
||||||
|
let (module, file) =
|
||||||
|
split_go_path("github.com/user/repo/@v/v4.1.2+incompatible.info").unwrap();
|
||||||
|
assert_eq!(module, "github.com/user/repo");
|
||||||
|
assert_eq!(file, "@v/v4.1.2+incompatible.info");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_pseudo_version() {
|
||||||
|
let (module, file) =
|
||||||
|
split_go_path("github.com/user/repo/@v/v0.0.0-20210101000000-abcdef123456.info")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(module, "github.com/user/repo");
|
||||||
|
assert_eq!(file, "@v/v0.0.0-20210101000000-abcdef123456.info");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_no_at() {
|
||||||
|
assert!(split_go_path("github.com/user/repo/v1.0.0").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_empty_module() {
|
||||||
|
assert!(split_go_path("/@v/list").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Path safety ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_safe_path_normal() {
|
||||||
|
assert!(is_safe_path("github.com/user/repo/@v/list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reject_traversal() {
|
||||||
|
assert!(!is_safe_path("../../etc/passwd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reject_absolute() {
|
||||||
|
assert!(!is_safe_path("/etc/passwd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reject_double_slash() {
|
||||||
|
assert!(!is_safe_path("github.com//evil/@v/list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reject_null() {
|
||||||
|
assert!(!is_safe_path("github.com/\0evil/@v/list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reject_empty() {
|
||||||
|
assert!(!is_safe_path(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Content-Type ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_content_type_info() {
|
||||||
|
assert_eq!(content_type_for("@v/v1.0.0.info"), "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_content_type_latest() {
|
||||||
|
assert_eq!(content_type_for("@latest"), "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_content_type_zip() {
|
||||||
|
assert_eq!(content_type_for("@v/v1.0.0.zip"), "application/zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_content_type_mod() {
|
||||||
|
assert_eq!(
|
||||||
|
content_type_for("@v/v1.0.0.mod"),
|
||||||
|
"text/plain; charset=utf-8"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_content_type_list() {
|
||||||
|
assert_eq!(content_type_for("@v/list"), "text/plain; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Artifact formatting ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_artifact_version() {
|
||||||
|
assert_eq!(
|
||||||
|
format_artifact("github.com/user/repo", "@v/v1.0.0.info"),
|
||||||
|
"github.com/user/repo@v1.0.0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_artifact_list() {
|
||||||
|
assert_eq!(
|
||||||
|
format_artifact("github.com/user/repo", "@v/list"),
|
||||||
|
"github.com/user/repo @v/list"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_artifact_latest() {
|
||||||
|
assert_eq!(
|
||||||
|
format_artifact("github.com/user/repo", "@latest"),
|
||||||
|
"github.com/user/repo @latest"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_artifact_zip() {
|
||||||
|
assert_eq!(
|
||||||
|
format_artifact("github.com/user/repo", "@v/v1.0.0.zip"),
|
||||||
|
"github.com/user/repo@v1.0.0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
mod cargo_registry;
|
mod cargo_registry;
|
||||||
pub mod docker;
|
pub mod docker;
|
||||||
pub mod docker_auth;
|
pub mod docker_auth;
|
||||||
|
mod go;
|
||||||
mod maven;
|
mod maven;
|
||||||
mod npm;
|
mod npm;
|
||||||
mod pypi;
|
mod pypi;
|
||||||
@@ -12,6 +13,7 @@ mod raw;
|
|||||||
pub use cargo_registry::routes as cargo_routes;
|
pub use cargo_registry::routes as cargo_routes;
|
||||||
pub use docker::routes as docker_routes;
|
pub use docker::routes as docker_routes;
|
||||||
pub use docker_auth::DockerAuth;
|
pub use docker_auth::DockerAuth;
|
||||||
|
pub use go::routes as go_routes;
|
||||||
pub use maven::routes as maven_routes;
|
pub use maven::routes as maven_routes;
|
||||||
pub use npm::routes as npm_routes;
|
pub use npm::routes as npm_routes;
|
||||||
pub use pypi::routes as pypi_routes;
|
pub use pypi::routes as pypi_routes;
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ pub struct RepoIndex {
|
|||||||
pub npm: RegistryIndex,
|
pub npm: RegistryIndex,
|
||||||
pub cargo: RegistryIndex,
|
pub cargo: RegistryIndex,
|
||||||
pub pypi: RegistryIndex,
|
pub pypi: RegistryIndex,
|
||||||
|
pub go: RegistryIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepoIndex {
|
impl RepoIndex {
|
||||||
@@ -90,6 +91,7 @@ impl RepoIndex {
|
|||||||
npm: RegistryIndex::new(),
|
npm: RegistryIndex::new(),
|
||||||
cargo: RegistryIndex::new(),
|
cargo: RegistryIndex::new(),
|
||||||
pypi: RegistryIndex::new(),
|
pypi: RegistryIndex::new(),
|
||||||
|
go: RegistryIndex::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +103,7 @@ impl RepoIndex {
|
|||||||
"npm" => self.npm.invalidate(),
|
"npm" => self.npm.invalidate(),
|
||||||
"cargo" => self.cargo.invalidate(),
|
"cargo" => self.cargo.invalidate(),
|
||||||
"pypi" => self.pypi.invalidate(),
|
"pypi" => self.pypi.invalidate(),
|
||||||
|
"go" => self.go.invalidate(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +116,7 @@ impl RepoIndex {
|
|||||||
"npm" => &self.npm,
|
"npm" => &self.npm,
|
||||||
"cargo" => &self.cargo,
|
"cargo" => &self.cargo,
|
||||||
"pypi" => &self.pypi,
|
"pypi" => &self.pypi,
|
||||||
|
"go" => &self.go,
|
||||||
_ => return Arc::new(Vec::new()),
|
_ => return Arc::new(Vec::new()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,6 +136,7 @@ impl RepoIndex {
|
|||||||
"npm" => build_npm_index(storage).await,
|
"npm" => build_npm_index(storage).await,
|
||||||
"cargo" => build_cargo_index(storage).await,
|
"cargo" => build_cargo_index(storage).await,
|
||||||
"pypi" => build_pypi_index(storage).await,
|
"pypi" => build_pypi_index(storage).await,
|
||||||
|
"go" => build_go_index(storage).await,
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
info!(registry = registry, count = data.len(), "Index rebuilt");
|
info!(registry = registry, count = data.len(), "Index rebuilt");
|
||||||
@@ -142,13 +147,14 @@ impl RepoIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get counts for stats (no rebuild, just current state)
|
/// Get counts for stats (no rebuild, just current state)
|
||||||
pub fn counts(&self) -> (usize, usize, usize, usize, usize) {
|
pub fn counts(&self) -> (usize, usize, usize, usize, usize, usize) {
|
||||||
(
|
(
|
||||||
self.docker.count(),
|
self.docker.count(),
|
||||||
self.maven.count(),
|
self.maven.count(),
|
||||||
self.npm.count(),
|
self.npm.count(),
|
||||||
self.cargo.count(),
|
self.cargo.count(),
|
||||||
self.pypi.count(),
|
self.pypi.count(),
|
||||||
|
self.go.count(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,6 +335,35 @@ async fn build_pypi_index(storage: &Storage) -> Vec<RepoInfo> {
|
|||||||
to_sorted_vec(packages)
|
to_sorted_vec(packages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn build_go_index(storage: &Storage) -> Vec<RepoInfo> {
|
||||||
|
let keys = storage.list("go/").await;
|
||||||
|
let mut modules: HashMap<String, (usize, u64, u64)> = HashMap::new();
|
||||||
|
|
||||||
|
for key in &keys {
|
||||||
|
if let Some(rest) = key.strip_prefix("go/") {
|
||||||
|
// Pattern: go/{module}/@v/{version}.zip
|
||||||
|
// Count .zip files as versions (authoritative artifacts)
|
||||||
|
if rest.contains("/@v/") && key.ends_with(".zip") {
|
||||||
|
// Extract module path: everything before /@v/
|
||||||
|
if let Some(pos) = rest.rfind("/@v/") {
|
||||||
|
let module = &rest[..pos];
|
||||||
|
let entry = modules.entry(module.to_string()).or_insert((0, 0, 0));
|
||||||
|
entry.0 += 1;
|
||||||
|
|
||||||
|
if let Some(meta) = storage.stat(key).await {
|
||||||
|
entry.1 += meta.size;
|
||||||
|
if meta.modified > entry.2 {
|
||||||
|
entry.2 = meta.modified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
to_sorted_vec(modules)
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert HashMap to sorted Vec<RepoInfo>
|
/// Convert HashMap to sorted Vec<RepoInfo>
|
||||||
fn to_sorted_vec(map: HashMap<String, (usize, u64, u64)>) -> Vec<RepoInfo> {
|
fn to_sorted_vec(map: HashMap<String, (usize, u64, u64)>) -> Vec<RepoInfo> {
|
||||||
let mut result: Vec<_> = map
|
let mut result: Vec<_> = map
|
||||||
@@ -482,8 +517,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_repo_index_new() {
|
fn test_repo_index_new() {
|
||||||
let idx = RepoIndex::new();
|
let idx = RepoIndex::new();
|
||||||
let (d, m, n, c, p) = idx.counts();
|
let (d, m, n, c, p, g) = idx.counts();
|
||||||
assert_eq!((d, m, n, c, p), (0, 0, 0, 0, 0));
|
assert_eq!((d, m, n, c, p, g), (0, 0, 0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -501,8 +536,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_repo_index_default() {
|
fn test_repo_index_default() {
|
||||||
let idx = RepoIndex::default();
|
let idx = RepoIndex::default();
|
||||||
let (d, m, n, c, p) = idx.counts();
|
let (d, m, n, c, p, g) = idx.counts();
|
||||||
assert_eq!((d, m, n, c, p), (0, 0, 0, 0, 0));
|
assert_eq!((d, m, n, c, p, g), (0, 0, 0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub struct RegistryStats {
|
|||||||
pub npm: usize,
|
pub npm: usize,
|
||||||
pub cargo: usize,
|
pub cargo: usize,
|
||||||
pub pypi: usize,
|
pub pypi: usize,
|
||||||
|
pub go: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -114,14 +115,16 @@ pub async fn api_stats(State(state): State<Arc<AppState>>) -> Json<RegistryStats
|
|||||||
let _ = state.repo_index.get("npm", &state.storage).await;
|
let _ = state.repo_index.get("npm", &state.storage).await;
|
||||||
let _ = state.repo_index.get("cargo", &state.storage).await;
|
let _ = state.repo_index.get("cargo", &state.storage).await;
|
||||||
let _ = state.repo_index.get("pypi", &state.storage).await;
|
let _ = state.repo_index.get("pypi", &state.storage).await;
|
||||||
|
let _ = state.repo_index.get("go", &state.storage).await;
|
||||||
|
|
||||||
let (docker, maven, npm, cargo, pypi) = state.repo_index.counts();
|
let (docker, maven, npm, cargo, pypi, go) = state.repo_index.counts();
|
||||||
Json(RegistryStats {
|
Json(RegistryStats {
|
||||||
docker,
|
docker,
|
||||||
maven,
|
maven,
|
||||||
npm,
|
npm,
|
||||||
cargo,
|
cargo,
|
||||||
pypi,
|
pypi,
|
||||||
|
go,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +135,7 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
let npm_repos = state.repo_index.get("npm", &state.storage).await;
|
let npm_repos = state.repo_index.get("npm", &state.storage).await;
|
||||||
let cargo_repos = state.repo_index.get("cargo", &state.storage).await;
|
let cargo_repos = state.repo_index.get("cargo", &state.storage).await;
|
||||||
let pypi_repos = state.repo_index.get("pypi", &state.storage).await;
|
let pypi_repos = state.repo_index.get("pypi", &state.storage).await;
|
||||||
|
let go_repos = state.repo_index.get("go", &state.storage).await;
|
||||||
|
|
||||||
// Calculate sizes from cached index
|
// Calculate sizes from cached index
|
||||||
let docker_size: u64 = docker_repos.iter().map(|r| r.size).sum();
|
let docker_size: u64 = docker_repos.iter().map(|r| r.size).sum();
|
||||||
@@ -139,7 +143,8 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
let npm_size: u64 = npm_repos.iter().map(|r| r.size).sum();
|
let npm_size: u64 = npm_repos.iter().map(|r| r.size).sum();
|
||||||
let cargo_size: u64 = cargo_repos.iter().map(|r| r.size).sum();
|
let cargo_size: u64 = cargo_repos.iter().map(|r| r.size).sum();
|
||||||
let pypi_size: u64 = pypi_repos.iter().map(|r| r.size).sum();
|
let pypi_size: u64 = pypi_repos.iter().map(|r| r.size).sum();
|
||||||
let total_storage = docker_size + maven_size + npm_size + cargo_size + pypi_size;
|
let go_size: u64 = go_repos.iter().map(|r| r.size).sum();
|
||||||
|
let total_storage = docker_size + maven_size + npm_size + cargo_size + pypi_size + go_size;
|
||||||
|
|
||||||
// Count total versions/tags, not just repositories
|
// Count total versions/tags, not just repositories
|
||||||
let docker_versions: usize = docker_repos.iter().map(|r| r.versions).sum();
|
let docker_versions: usize = docker_repos.iter().map(|r| r.versions).sum();
|
||||||
@@ -147,8 +152,13 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
let npm_versions: usize = npm_repos.iter().map(|r| r.versions).sum();
|
let npm_versions: usize = npm_repos.iter().map(|r| r.versions).sum();
|
||||||
let cargo_versions: usize = cargo_repos.iter().map(|r| r.versions).sum();
|
let cargo_versions: usize = cargo_repos.iter().map(|r| r.versions).sum();
|
||||||
let pypi_versions: usize = pypi_repos.iter().map(|r| r.versions).sum();
|
let pypi_versions: usize = pypi_repos.iter().map(|r| r.versions).sum();
|
||||||
let total_artifacts =
|
let go_versions: usize = go_repos.iter().map(|r| r.versions).sum();
|
||||||
docker_versions + maven_versions + npm_versions + cargo_versions + pypi_versions;
|
let total_artifacts = docker_versions
|
||||||
|
+ maven_versions
|
||||||
|
+ npm_versions
|
||||||
|
+ cargo_versions
|
||||||
|
+ pypi_versions
|
||||||
|
+ go_versions;
|
||||||
|
|
||||||
let global_stats = GlobalStats {
|
let global_stats = GlobalStats {
|
||||||
downloads: state.metrics.downloads.load(Ordering::Relaxed),
|
downloads: state.metrics.downloads.load(Ordering::Relaxed),
|
||||||
@@ -194,6 +204,13 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
uploads: 0,
|
uploads: 0,
|
||||||
size_bytes: pypi_size,
|
size_bytes: pypi_size,
|
||||||
},
|
},
|
||||||
|
RegistryCardStats {
|
||||||
|
name: "go".to_string(),
|
||||||
|
artifact_count: go_versions,
|
||||||
|
downloads: state.metrics.get_registry_downloads("go"),
|
||||||
|
uploads: 0,
|
||||||
|
size_bytes: go_size,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let mount_points = vec![
|
let mount_points = vec![
|
||||||
@@ -227,6 +244,11 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
mount_path: "/simple/".to_string(),
|
mount_path: "/simple/".to_string(),
|
||||||
proxy_upstream: state.config.pypi.proxy.clone(),
|
proxy_upstream: state.config.pypi.proxy.clone(),
|
||||||
},
|
},
|
||||||
|
MountPoint {
|
||||||
|
registry: "Go".to_string(),
|
||||||
|
mount_path: "/go/".to_string(),
|
||||||
|
proxy_upstream: state.config.go.proxy.clone(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let activity = state.activity.recent(20);
|
let activity = state.activity.recent(20);
|
||||||
@@ -373,12 +395,24 @@ pub async fn get_registry_stats(storage: &Storage) -> RegistryStats {
|
|||||||
.collect::<HashSet<_>>()
|
.collect::<HashSet<_>>()
|
||||||
.len();
|
.len();
|
||||||
|
|
||||||
|
let go = all_keys
|
||||||
|
.iter()
|
||||||
|
.filter(|k| k.starts_with("go/") && k.ends_with(".zip"))
|
||||||
|
.filter_map(|k| {
|
||||||
|
let rest = k.strip_prefix("go/")?;
|
||||||
|
let pos = rest.rfind("/@v/")?;
|
||||||
|
Some(rest[..pos].to_string())
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.len();
|
||||||
|
|
||||||
RegistryStats {
|
RegistryStats {
|
||||||
docker,
|
docker,
|
||||||
maven,
|
maven,
|
||||||
npm,
|
npm,
|
||||||
cargo,
|
cargo,
|
||||||
pypi,
|
pypi,
|
||||||
|
go,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,6 +908,32 @@ pub async fn get_pypi_detail(storage: &Storage, name: &str) -> PackageDetail {
|
|||||||
PackageDetail { versions }
|
PackageDetail { versions }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_go_detail(storage: &Storage, module: &str) -> PackageDetail {
|
||||||
|
let prefix = format!("go/{}/@v/", module);
|
||||||
|
let keys = storage.list(&prefix).await;
|
||||||
|
|
||||||
|
let mut versions = Vec::new();
|
||||||
|
for key in keys.iter().filter(|k| k.ends_with(".zip")) {
|
||||||
|
if let Some(rest) = key.strip_prefix(&prefix) {
|
||||||
|
if let Some(version) = rest.strip_suffix(".zip") {
|
||||||
|
let (size, published) = if let Some(meta) = storage.stat(key).await {
|
||||||
|
(meta.size, format_timestamp(meta.modified))
|
||||||
|
} else {
|
||||||
|
(0, "N/A".to_string())
|
||||||
|
};
|
||||||
|
versions.push(VersionInfo {
|
||||||
|
version: version.to_string(),
|
||||||
|
size,
|
||||||
|
published,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
versions.sort_by(|a, b| b.version.cmp(&a.version));
|
||||||
|
PackageDetail { versions }
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_pypi_version(name: &str, filename: &str) -> Option<String> {
|
fn extract_pypi_version(name: &str, filename: &str) -> Option<String> {
|
||||||
// Handle both .tar.gz and .whl files
|
// Handle both .tar.gz and .whl files
|
||||||
let clean_name = name.replace('-', "_");
|
let clean_name = name.replace('-', "_");
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ fn sidebar_dark(active_page: Option<&str>, t: &Translations) -> String {
|
|||||||
let docker_icon = r#"<path fill="currentColor" d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.186m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186h-2.12a.186.186 0 00-.185.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>"#;
|
let docker_icon = r#"<path fill="currentColor" d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.186m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186h-2.12a.186.186 0 00-.185.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>"#;
|
||||||
let maven_icon = r#"<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>"#;
|
let maven_icon = r#"<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>"#;
|
||||||
let npm_icon = r#"<path fill="currentColor" d="M0 7.334v8h6.666v1.332H12v-1.332h12v-8H0zm6.666 6.664H5.334v-4H3.999v4H1.335V8.667h5.331v5.331zm4 0v1.336H8.001V8.667h5.334v5.332h-2.669v-.001zm12.001 0h-1.33v-4h-1.336v4h-1.335v-4h-1.33v4h-2.671V8.667h8.002v5.331zM10.665 10H12v2.667h-1.335V10z"/>"#;
|
let npm_icon = r#"<path fill="currentColor" d="M0 7.334v8h6.666v1.332H12v-1.332h12v-8H0zm6.666 6.664H5.334v-4H3.999v4H1.335V8.667h5.331v5.331zm4 0v1.336H8.001V8.667h5.334v5.332h-2.669v-.001zm12.001 0h-1.33v-4h-1.336v4h-1.335v-4h-1.33v4h-2.671V8.667h8.002v5.331zM10.665 10H12v2.667h-1.335V10z"/>"#;
|
||||||
let cargo_icon = r#"<path fill="currentColor" d="M20 8h-3V4H3c-1.1 0-2 .9-2 2v11h2c0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h2v-5l-3-4zM6 18.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5-9l1.96 2.5H17V9.5h2.5zm-1.5 9c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>"#;
|
let cargo_icon = r#"<path fill="currentColor" d="M6 2h12a1 1 0 011 1v8a1 1 0 01-1 1H6a1 1 0 01-1-1V3a1 1 0 011-1zm0 2v2h12V4H6zm0 3v2h12V7H6zM2 14h8a1 1 0 011 1v6a1 1 0 01-1 1H2a1 1 0 01-1-1v-6a1 1 0 011-1zm0 2v1.5h8V16H2zM14 14h8a1 1 0 011 1v6a1 1 0 01-1 1h-8a1 1 0 01-1-1v-6a1 1 0 011-1zm0 2v1.5h8V16h-8z"/>"#;
|
||||||
let pypi_icon = r#"<path fill="currentColor" d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.83l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.23l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05L0 11.97l.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.24l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05 1.07.13zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09-.33.22zM21.1 6.11l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01.21.03zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08-.33.23z"/>"#;
|
let pypi_icon = r#"<path fill="currentColor" d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.83l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.23l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05L0 11.97l.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.24l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05 1.07.13zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09-.33.22zM21.1 6.11l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01.21.03zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08-.33.23z"/>"#;
|
||||||
|
|
||||||
// Dashboard label is translated, registry names stay as-is
|
// Dashboard label is translated, registry names stay as-is
|
||||||
@@ -109,6 +109,13 @@ fn sidebar_dark(active_page: Option<&str>, t: &Translations) -> String {
|
|||||||
("npm", "/ui/npm", "npm", npm_icon, false),
|
("npm", "/ui/npm", "npm", npm_icon, false),
|
||||||
("cargo", "/ui/cargo", "Cargo", cargo_icon, false),
|
("cargo", "/ui/cargo", "Cargo", cargo_icon, false),
|
||||||
("pypi", "/ui/pypi", "PyPI", pypi_icon, false),
|
("pypi", "/ui/pypi", "PyPI", pypi_icon, false),
|
||||||
|
(
|
||||||
|
"go",
|
||||||
|
"/ui/go",
|
||||||
|
"Go",
|
||||||
|
r#"<path fill="currentColor" d="M2.64 9.56s.24-.14.65-.38c.41-.24.97-.5 1.63-.7A7.85 7.85 0 017.53 8c.86 0 1.67.17 2.37.52.7.35 1.26.87 1.63 1.51.37.64.54 1.41.54 2.27v.2h-2.7v-.16c0-.47-.09-.86-.28-1.15a1.7 1.7 0 00-.77-.67 2.7 2.7 0 00-1.14-.22c-.56 0-1.06.13-1.46.4-.41.27-.72.66-.93 1.16-.21.5-.31 1.1-.31 1.8 0 .69.1 1.28.32 1.78.21.5.53.88.94 1.15.41.27.9.4 1.47.4.38 0 .73-.06 1.04-.17.31-.12.56-.29.74-.52.19-.23.29-.51.29-.84v-.14H7.15v-1.76h5.07v1.3c0 .8-.17 1.48-.52 2.04a3.46 3.46 0 01-1.5 1.3c-.66.3-1.44.45-2.35.45-.99 0-1.87-.18-2.63-.55a4.2 4.2 0 01-1.77-1.59C3.15 14.82 3 13.94 3 12.89v-.28c0-1.04.16-1.93.48-2.65a3.08 3.08 0 01-.84-.4zm12.1-1.34c.92 0 1.74.18 2.44.55a3.96 3.96 0 011.66 1.59c.4.7.6 1.54.6 2.53v.28c0 .99-.2 1.83-.6 2.53a3.96 3.96 0 01-1.66 1.59c-.7.37-1.52.55-2.44.55s-1.74-.18-2.44-.55a3.96 3.96 0 01-1.66-1.59c-.4-.7-.6-1.54-.6-2.53v-.28c0-.99.2-1.83.6-2.53a3.96 3.96 0 011.66-1.59c.7-.37 1.52-.55 2.44-.55zm0 2.12c-.44 0-.82.12-1.14.37-.32.24-.56.6-.73 1.06-.17.46-.26 1.01-.26 1.65v.28c0 .64.09 1.19.26 1.65.17.46.41.82.73 1.06.32.25.7.37 1.14.37.44 0 .82-.12 1.14-.37.32-.24.56-.6.73-1.06.17-.46.26-1.01.26-1.65v-.28c0-.64-.09-1.19-.26-1.65a2.17 2.17 0 00-.73-1.06 1.78 1.78 0 00-1.14-.37z"/>"#,
|
||||||
|
false,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
let nav_html: String = nav_items.iter().map(|(id, href, label, icon_path, is_stroke)| {
|
let nav_html: String = nav_items.iter().map(|(id, href, label, icon_path, is_stroke)| {
|
||||||
@@ -277,15 +284,15 @@ pub fn render_registry_card(
|
|||||||
) -> String {
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
r##"
|
r##"
|
||||||
<a href="{}" id="registry-{}" class="block bg-[#1e293b] rounded-lg border border-slate-700 p-4 md:p-6 hover:border-blue-400 transition-all">
|
<a href="{}" id="registry-{}" class="block bg-[#1e293b] rounded-lg border border-slate-700 p-3 hover:border-blue-400 transition-all">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<svg class="w-8 h-8 text-slate-400" fill="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 text-slate-400" fill="currentColor" viewBox="0 0 24 24">
|
||||||
{}
|
{}
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-xs font-medium text-green-400 bg-green-400/10 px-2 py-1 rounded-full">{}</span>
|
<span class="text-[10px] font-medium text-green-400 bg-green-400/10 px-1.5 py-0.5 rounded-full">{}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-lg font-semibold text-slate-200 mb-2">{}</div>
|
<div class="text-sm font-semibold text-slate-200 mb-2">{}</div>
|
||||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
<div class="grid grid-cols-2 gap-1 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-slate-500">{}</span>
|
<span class="text-slate-500">{}</span>
|
||||||
<div class="text-slate-300 font-medium">{}</div>
|
<div class="text-slate-300 font-medium">{}</div>
|
||||||
@@ -490,7 +497,7 @@ fn sidebar(active_page: Option<&str>) -> String {
|
|||||||
let docker_icon = r#"<path fill="currentColor" d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.186m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186h-2.12a.186.186 0 00-.185.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>"#;
|
let docker_icon = r#"<path fill="currentColor" d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.186m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186h-2.12a.186.186 0 00-.185.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>"#;
|
||||||
let maven_icon = r#"<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>"#;
|
let maven_icon = r#"<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>"#;
|
||||||
let npm_icon = r#"<path fill="currentColor" d="M0 7.334v8h6.666v1.332H12v-1.332h12v-8H0zm6.666 6.664H5.334v-4H3.999v4H1.335V8.667h5.331v5.331zm4 0v1.336H8.001V8.667h5.334v5.332h-2.669v-.001zm12.001 0h-1.33v-4h-1.336v4h-1.335v-4h-1.33v4h-2.671V8.667h8.002v5.331zM10.665 10H12v2.667h-1.335V10z"/>"#;
|
let npm_icon = r#"<path fill="currentColor" d="M0 7.334v8h6.666v1.332H12v-1.332h12v-8H0zm6.666 6.664H5.334v-4H3.999v4H1.335V8.667h5.331v5.331zm4 0v1.336H8.001V8.667h5.334v5.332h-2.669v-.001zm12.001 0h-1.33v-4h-1.336v4h-1.335v-4h-1.33v4h-2.671V8.667h8.002v5.331zM10.665 10H12v2.667h-1.335V10z"/>"#;
|
||||||
let cargo_icon = r#"<path fill="currentColor" d="M20 8h-3V4H3c-1.1 0-2 .9-2 2v11h2c0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h2v-5l-3-4zM6 18.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5-9l1.96 2.5H17V9.5h2.5zm-1.5 9c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>"#;
|
let cargo_icon = r#"<path fill="currentColor" d="M6 2h12a1 1 0 011 1v8a1 1 0 01-1 1H6a1 1 0 01-1-1V3a1 1 0 011-1zm0 2v2h12V4H6zm0 3v2h12V7H6zM2 14h8a1 1 0 011 1v6a1 1 0 01-1 1H2a1 1 0 01-1-1v-6a1 1 0 011-1zm0 2v1.5h8V16H2zM14 14h8a1 1 0 011 1v6a1 1 0 01-1 1h-8a1 1 0 01-1-1v-6a1 1 0 011-1zm0 2v1.5h8V16h-8z"/>"#;
|
||||||
let pypi_icon = r#"<path fill="currentColor" d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.83l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.23l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05L0 11.97l.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.24l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05 1.07.13zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09-.33.22zM21.1 6.11l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01.21.03zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08-.33.23z"/>"#;
|
let pypi_icon = r#"<path fill="currentColor" d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.83l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.23l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05L0 11.97l.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.24l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05 1.07.13zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09-.33.22zM21.1 6.11l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01.21.03zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08-.33.23z"/>"#;
|
||||||
|
|
||||||
let nav_items = [
|
let nav_items = [
|
||||||
@@ -506,6 +513,13 @@ fn sidebar(active_page: Option<&str>) -> String {
|
|||||||
("npm", "/ui/npm", "npm", npm_icon, false),
|
("npm", "/ui/npm", "npm", npm_icon, false),
|
||||||
("cargo", "/ui/cargo", "Cargo", cargo_icon, false),
|
("cargo", "/ui/cargo", "Cargo", cargo_icon, false),
|
||||||
("pypi", "/ui/pypi", "PyPI", pypi_icon, false),
|
("pypi", "/ui/pypi", "PyPI", pypi_icon, false),
|
||||||
|
(
|
||||||
|
"go",
|
||||||
|
"/ui/go",
|
||||||
|
"Go",
|
||||||
|
r#"<path fill="currentColor" d="M2.64 9.56s.24-.14.65-.38c.41-.24.97-.5 1.63-.7A7.85 7.85 0 017.53 8c.86 0 1.67.17 2.37.52.7.35 1.26.87 1.63 1.51.37.64.54 1.41.54 2.27v.2h-2.7v-.16c0-.47-.09-.86-.28-1.15a1.7 1.7 0 00-.77-.67 2.7 2.7 0 00-1.14-.22c-.56 0-1.06.13-1.46.4-.41.27-.72.66-.93 1.16-.21.5-.31 1.1-.31 1.8 0 .69.1 1.28.32 1.78.21.5.53.88.94 1.15.41.27.9.4 1.47.4.38 0 .73-.06 1.04-.17.31-.12.56-.29.74-.52.19-.23.29-.51.29-.84v-.14H7.15v-1.76h5.07v1.3c0 .8-.17 1.48-.52 2.04a3.46 3.46 0 01-1.5 1.3c-.66.3-1.44.45-2.35.45-.99 0-1.87-.18-2.63-.55a4.2 4.2 0 01-1.77-1.59C3.15 14.82 3 13.94 3 12.89v-.28c0-1.04.16-1.93.48-2.65a3.08 3.08 0 01-.84-.4zm12.1-1.34c.92 0 1.74.18 2.44.55a3.96 3.96 0 011.66 1.59c.4.7.6 1.54.6 2.53v.28c0 .99-.2 1.83-.6 2.53a3.96 3.96 0 01-1.66 1.59c-.7.37-1.52.55-2.44.55s-1.74-.18-2.44-.55a3.96 3.96 0 01-1.66-1.59c-.4-.7-.6-1.54-.6-2.53v-.28c0-.99.2-1.83.6-2.53a3.96 3.96 0 011.66-1.59c.7-.37 1.52-.55 2.44-.55zm0 2.12c-.44 0-.82.12-1.14.37-.32.24-.56.6-.73 1.06-.17.46-.26 1.01-.26 1.65v.28c0 .64.09 1.19.26 1.65.17.46.41.82.73 1.06.32.25.7.37 1.14.37.44 0 .82-.12 1.14-.37.32-.24.56-.6.73-1.06.17-.46.26-1.01.26-1.65v-.28c0-.64-.09-1.19-.26-1.65a2.17 2.17 0 00-.73-1.06 1.78 1.78 0 00-1.14-.37z"/>"#,
|
||||||
|
false,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
let nav_html: String = nav_items.iter().map(|(id, href, label, icon_path, is_stroke)| {
|
let nav_html: String = nav_items.iter().map(|(id, href, label, icon_path, is_stroke)| {
|
||||||
@@ -613,7 +627,8 @@ pub mod icons {
|
|||||||
pub const DOCKER: &str = r#"<path fill="currentColor" d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.186m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186h-2.12a.186.186 0 00-.185.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>"#;
|
pub const DOCKER: &str = r#"<path fill="currentColor" d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.186m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186h-2.12a.186.186 0 00-.185.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>"#;
|
||||||
pub const MAVEN: &str = r#"<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>"#;
|
pub const MAVEN: &str = r#"<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>"#;
|
||||||
pub const NPM: &str = r#"<path fill="currentColor" d="M0 7.334v8h6.666v1.332H12v-1.332h12v-8H0zm6.666 6.664H5.334v-4H3.999v4H1.335V8.667h5.331v5.331zm4 0v1.336H8.001V8.667h5.334v5.332h-2.669v-.001zm12.001 0h-1.33v-4h-1.336v4h-1.335v-4h-1.33v4h-2.671V8.667h8.002v5.331zM10.665 10H12v2.667h-1.335V10z"/>"#;
|
pub const NPM: &str = r#"<path fill="currentColor" d="M0 7.334v8h6.666v1.332H12v-1.332h12v-8H0zm6.666 6.664H5.334v-4H3.999v4H1.335V8.667h5.331v5.331zm4 0v1.336H8.001V8.667h5.334v5.332h-2.669v-.001zm12.001 0h-1.33v-4h-1.336v4h-1.335v-4h-1.33v4h-2.671V8.667h8.002v5.331zM10.665 10H12v2.667h-1.335V10z"/>"#;
|
||||||
pub const CARGO: &str = r#"<path fill="currentColor" d="M20 8h-3V4H3c-1.1 0-2 .9-2 2v11h2c0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h2v-5l-3-4zM6 18.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5-9l1.96 2.5H17V9.5h2.5zm-1.5 9c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>"#;
|
pub const CARGO: &str = r#"<path fill="currentColor" d="M6 2h12a1 1 0 011 1v8a1 1 0 01-1 1H6a1 1 0 01-1-1V3a1 1 0 011-1zm0 2v2h12V4H6zm0 3v2h12V7H6zM2 14h8a1 1 0 011 1v6a1 1 0 01-1 1H2a1 1 0 01-1-1v-6a1 1 0 011-1zm0 2v1.5h8V16H2zM14 14h8a1 1 0 011 1v6a1 1 0 01-1 1h-8a1 1 0 01-1-1v-6a1 1 0 011-1zm0 2v1.5h8V16h-8z"/>"#;
|
||||||
|
pub const GO: &str = r#"<path fill="currentColor" d="M2.64 9.56s.24-.14.65-.38c.41-.24.97-.5 1.63-.7A7.85 7.85 0 017.53 8c.86 0 1.67.17 2.37.52.7.35 1.26.87 1.63 1.51.37.64.54 1.41.54 2.27v.2h-2.7v-.16c0-.47-.09-.86-.28-1.15a1.7 1.7 0 00-.77-.67 2.7 2.7 0 00-1.14-.22c-.56 0-1.06.13-1.46.4-.41.27-.72.66-.93 1.16-.21.5-.31 1.1-.31 1.8 0 .69.1 1.28.32 1.78.21.5.53.88.94 1.15.41.27.9.4 1.47.4.38 0 .73-.06 1.04-.17.31-.12.56-.29.74-.52.19-.23.29-.51.29-.84v-.14H7.15v-1.76h5.07v1.3c0 .8-.17 1.48-.52 2.04a3.46 3.46 0 01-1.5 1.3c-.66.3-1.44.45-2.35.45-.99 0-1.87-.18-2.63-.55a4.2 4.2 0 01-1.77-1.59C3.15 14.82 3 13.94 3 12.89v-.28c0-1.04.16-1.93.48-2.65a3.08 3.08 0 01-.84-.4zm12.1-1.34c.92 0 1.74.18 2.44.55a3.96 3.96 0 011.66 1.59c.4.7.6 1.54.6 2.53v.28c0 .99-.2 1.83-.6 2.53a3.96 3.96 0 01-1.66 1.59c-.7.37-1.52.55-2.44.55s-1.74-.18-2.44-.55a3.96 3.96 0 01-1.66-1.59c-.4-.7-.6-1.54-.6-2.53v-.28c0-.99.2-1.83.6-2.53a3.96 3.96 0 011.66-1.59c.7-.37 1.52-.55 2.44-.55zm0 2.12c-.44 0-.82.12-1.14.37-.32.24-.56.6-.73 1.06-.17.46-.26 1.01-.26 1.65v.28c0 .64.09 1.19.26 1.65.17.46.41.82.73 1.06.32.25.7.37 1.14.37.44 0 .82-.12 1.14-.37.32-.24.56-.6.73-1.06.17-.46.26-1.01.26-1.65v-.28c0-.64-.09-1.19-.26-1.65a2.17 2.17 0 00-.73-1.06 1.78 1.78 0 00-1.14-.37z"/>"#;
|
||||||
pub const PYPI: &str = r#"<path fill="currentColor" d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.83l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.23l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05L0 11.97l.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.24l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05 1.07.13zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09-.33.22zM21.1 6.11l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01.21.03zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08-.33.23z"/>"#;
|
pub const PYPI: &str = r#"<path fill="currentColor" d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.83l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.23l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05L0 11.97l.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.24l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05 1.07.13zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09-.33.22zM21.1 6.11l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01.21.03zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08-.33.23z"/>"#;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ pub fn routes() -> Router<Arc<AppState>> {
|
|||||||
.route("/ui/cargo/{name}", get(cargo_detail))
|
.route("/ui/cargo/{name}", get(cargo_detail))
|
||||||
.route("/ui/pypi", get(pypi_list))
|
.route("/ui/pypi", get(pypi_list))
|
||||||
.route("/ui/pypi/{name}", get(pypi_detail))
|
.route("/ui/pypi/{name}", get(pypi_detail))
|
||||||
|
.route("/ui/go", get(go_list))
|
||||||
|
.route("/ui/go/{*name}", get(go_detail))
|
||||||
// API endpoints for HTMX
|
// API endpoints for HTMX
|
||||||
.route("/api/ui/stats", get(api_stats))
|
.route("/api/ui/stats", get(api_stats))
|
||||||
.route("/api/ui/dashboard", get(api_dashboard))
|
.route("/api/ui/dashboard", get(api_dashboard))
|
||||||
@@ -298,3 +300,41 @@ async fn pypi_detail(
|
|||||||
let detail = get_pypi_detail(&state.storage, &name).await;
|
let detail = get_pypi_detail(&state.storage, &name).await;
|
||||||
Html(render_package_detail("pypi", &name, &detail, lang))
|
Html(render_package_detail("pypi", &name, &detail, lang))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Go pages
|
||||||
|
async fn go_list(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Query(query): Query<ListQuery>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let lang = extract_lang_from_list(&query, headers.get("cookie").and_then(|v| v.to_str().ok()));
|
||||||
|
let page = query.page.unwrap_or(1).max(1);
|
||||||
|
let limit = query.limit.unwrap_or(DEFAULT_PAGE_SIZE).min(100);
|
||||||
|
|
||||||
|
let all_modules = state.repo_index.get("go", &state.storage).await;
|
||||||
|
let (modules, total) = paginate(&all_modules, page, limit);
|
||||||
|
|
||||||
|
Html(render_registry_list_paginated(
|
||||||
|
"go",
|
||||||
|
"Go Modules",
|
||||||
|
&modules,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
lang,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn go_detail(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(name): Path<String>,
|
||||||
|
Query(query): Query<LangQuery>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let lang = extract_lang(
|
||||||
|
&Query(query),
|
||||||
|
headers.get("cookie").and_then(|v| v.to_str().ok()),
|
||||||
|
);
|
||||||
|
let detail = get_go_detail(&state.storage, &name).await;
|
||||||
|
Html(render_package_detail("go", &name, &detail, lang))
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,22 +24,8 @@ pub fn render_dashboard(data: &DashboardResponse, lang: Lang) -> String {
|
|||||||
.registry_stats
|
.registry_stats
|
||||||
.iter()
|
.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
let icon = match r.name.as_str() {
|
let icon = get_registry_icon(&r.name);
|
||||||
"docker" => icons::DOCKER,
|
let display_name = get_registry_title(&r.name);
|
||||||
"maven" => icons::MAVEN,
|
|
||||||
"npm" => icons::NPM,
|
|
||||||
"cargo" => icons::CARGO,
|
|
||||||
"pypi" => icons::PYPI,
|
|
||||||
_ => icons::DOCKER,
|
|
||||||
};
|
|
||||||
let display_name = match r.name.as_str() {
|
|
||||||
"docker" => "Docker",
|
|
||||||
"maven" => "Maven",
|
|
||||||
"npm" => "npm",
|
|
||||||
"cargo" => "Cargo",
|
|
||||||
"pypi" => "PyPI",
|
|
||||||
_ => &r.name,
|
|
||||||
};
|
|
||||||
render_registry_card(
|
render_registry_card(
|
||||||
display_name,
|
display_name,
|
||||||
icon,
|
icon,
|
||||||
@@ -155,7 +141,7 @@ pub fn render_dashboard(data: &DashboardResponse, lang: Lang) -> String {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 mb-6">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-6">
|
||||||
{}
|
{}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -669,6 +655,7 @@ pub fn render_package_detail(
|
|||||||
"pip install {} --index-url http://127.0.0.1:4000/simple",
|
"pip install {} --index-url http://127.0.0.1:4000/simple",
|
||||||
name
|
name
|
||||||
),
|
),
|
||||||
|
"go" => format!("GOPROXY=http://127.0.0.1:4000/go go get {}", name),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -835,6 +822,7 @@ fn get_registry_icon(registry_type: &str) -> &'static str {
|
|||||||
"npm" => icons::NPM,
|
"npm" => icons::NPM,
|
||||||
"cargo" => icons::CARGO,
|
"cargo" => icons::CARGO,
|
||||||
"pypi" => icons::PYPI,
|
"pypi" => icons::PYPI,
|
||||||
|
"go" => icons::GO,
|
||||||
_ => {
|
_ => {
|
||||||
r#"<path fill="currentColor" d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>"#
|
r#"<path fill="currentColor" d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>"#
|
||||||
}
|
}
|
||||||
@@ -848,6 +836,7 @@ fn get_registry_title(registry_type: &str) -> &'static str {
|
|||||||
"npm" => "npm Registry",
|
"npm" => "npm Registry",
|
||||||
"cargo" => "Cargo Registry",
|
"cargo" => "Cargo Registry",
|
||||||
"pypi" => "PyPI Repository",
|
"pypi" => "PyPI Repository",
|
||||||
|
"go" => "Go Modules",
|
||||||
_ => "Registry",
|
_ => "Registry",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user