mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 12:40:31 +00:00
docs: add Go module proxy support to README (#62)
* docs: add Go module proxy to README, update dashboard GIF - Add Go Modules to supported registries table - Add Go usage example (GOPROXY) - Add Go config.toml example - Add /go/ endpoint to endpoints table - Update dashboard GIF with 6 registry cards in one row - Fix registries count: 6 package registries * feat(ui): add Raw storage to dashboard, sidebar, and list pages - Raw Storage card on dashboard with file count and size - Raw in sidebar navigation with file icon - Raw list and detail pages (/ui/raw) - Raw mount point in mount points table - Grid updated to 7 columns for all registry cards - README: 7 registries, add Go module proxy docs * docs: add product badges (release, image size, downloads)
This commit is contained in:
BIN
.github/assets/dashboard.gif
vendored
BIN
.github/assets/dashboard.gif
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 124 KiB |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1304,7 +1304,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nora-registry"
|
name = "nora-registry"
|
||||||
version = "0.2.35"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -15,10 +15,15 @@ Open [http://localhost:4000/ui/](http://localhost:4000/ui/) — your registry is
|
|||||||
## Why NORA
|
## Why NORA
|
||||||
|
|
||||||
- **Zero-config** — single 32 MB binary, no database, no dependencies. `docker run` and it works.
|
- **Zero-config** — single 32 MB binary, no database, no dependencies. `docker run` and it works.
|
||||||
- **Production-tested** — Docker, Maven, npm, PyPI, Cargo, Raw. Used in real CI/CD with ArgoCD, Buildx cache, and air-gapped environments.
|
- **Production-tested** — Docker, Maven, npm, PyPI, Cargo, Go, Raw. Used in real CI/CD with ArgoCD, Buildx cache, and air-gapped environments.
|
||||||
- **Secure by default** — [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/getnora-io/nora), signed releases, SBOM, fuzz testing, 200+ unit tests.
|
- **Secure by default** — [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/getnora-io/nora), signed releases, SBOM, fuzz testing, 200+ unit tests.
|
||||||
|
|
||||||
**32 MB** binary | **< 100 MB** RAM | **3s** startup | **6** registries
|
[](https://github.com/getnora-io/nora/releases)
|
||||||
|
[](https://github.com/getnora-io/nora/pkgs/container/nora)
|
||||||
|
[](https://github.com/getnora-io/nora/releases)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
**32 MB** binary | **< 100 MB** RAM | **3s** startup | **7** registries
|
||||||
|
|
||||||
> Used in production at [DevIT Academy](https://github.com/devitway) since January 2026 for Docker images, Maven artifacts, and npm packages.
|
> Used in production at [DevIT Academy](https://github.com/devitway) since January 2026 for Docker images, Maven artifacts, and npm packages.
|
||||||
|
|
||||||
@@ -31,6 +36,7 @@ Open [http://localhost:4000/ui/](http://localhost:4000/ui/) — your registry is
|
|||||||
| npm | `/npm/` | npmjs.org, custom | ✓ |
|
| npm | `/npm/` | npmjs.org, custom | ✓ |
|
||||||
| Cargo | `/cargo/` | — | ✓ |
|
| Cargo | `/cargo/` | — | ✓ |
|
||||||
| PyPI | `/simple/` | pypi.org, custom | ✓ |
|
| PyPI | `/simple/` | pypi.org, custom | ✓ |
|
||||||
|
| Go Modules | `/go/` | proxy.golang.org, custom | ✓ |
|
||||||
| Raw files | `/raw/` | — | ✓ |
|
| Raw files | `/raw/` | — | ✓ |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -82,6 +88,12 @@ npm config set registry http://localhost:4000/npm/
|
|||||||
npm publish
|
npm publish
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Go Modules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOPROXY=http://localhost:4000/go go get golang.org/x/text@latest
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Web UI** — dashboard with search, browse, i18n (EN/RU)
|
- **Web UI** — dashboard with search, browse, i18n (EN/RU)
|
||||||
@@ -154,6 +166,9 @@ proxy_timeout = 60
|
|||||||
|
|
||||||
[[docker.upstreams]]
|
[[docker.upstreams]]
|
||||||
url = "https://registry-1.docker.io"
|
url = "https://registry-1.docker.io"
|
||||||
|
|
||||||
|
[go]
|
||||||
|
proxy = "https://proxy.golang.org"
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
@@ -181,6 +196,7 @@ nora mirror # Sync packages for offline use
|
|||||||
| `/npm/` | npm |
|
| `/npm/` | npm |
|
||||||
| `/cargo/` | Cargo |
|
| `/cargo/` | Cargo |
|
||||||
| `/simple/` | PyPI |
|
| `/simple/` | PyPI |
|
||||||
|
| `/go/` | Go Modules |
|
||||||
|
|
||||||
## TLS / HTTPS
|
## TLS / HTTPS
|
||||||
|
|
||||||
@@ -220,7 +236,6 @@ See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|||||||
[](https://www.bestpractices.dev/projects/12207)
|
[](https://www.bestpractices.dev/projects/12207)
|
||||||
[](https://github.com/getnora-io/nora/actions/workflows/ci.yml)
|
[](https://github.com/getnora-io/nora/actions/workflows/ci.yml)
|
||||||
[](https://github.com/getnora-io/nora/actions)
|
[](https://github.com/getnora-io/nora/actions)
|
||||||
[](LICENSE)
|
|
||||||
|
|
||||||
- **Signed releases** — every release is signed with [cosign](https://github.com/sigstore/cosign)
|
- **Signed releases** — every release is signed with [cosign](https://github.com/sigstore/cosign)
|
||||||
- **SBOM** — SPDX + CycloneDX in every release
|
- **SBOM** — SPDX + CycloneDX in every release
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ pub struct RepoIndex {
|
|||||||
pub cargo: RegistryIndex,
|
pub cargo: RegistryIndex,
|
||||||
pub pypi: RegistryIndex,
|
pub pypi: RegistryIndex,
|
||||||
pub go: RegistryIndex,
|
pub go: RegistryIndex,
|
||||||
|
pub raw: RegistryIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepoIndex {
|
impl RepoIndex {
|
||||||
@@ -92,6 +93,7 @@ impl RepoIndex {
|
|||||||
cargo: RegistryIndex::new(),
|
cargo: RegistryIndex::new(),
|
||||||
pypi: RegistryIndex::new(),
|
pypi: RegistryIndex::new(),
|
||||||
go: RegistryIndex::new(),
|
go: RegistryIndex::new(),
|
||||||
|
raw: RegistryIndex::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +106,7 @@ impl RepoIndex {
|
|||||||
"cargo" => self.cargo.invalidate(),
|
"cargo" => self.cargo.invalidate(),
|
||||||
"pypi" => self.pypi.invalidate(),
|
"pypi" => self.pypi.invalidate(),
|
||||||
"go" => self.go.invalidate(),
|
"go" => self.go.invalidate(),
|
||||||
|
"raw" => self.raw.invalidate(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,6 +120,7 @@ impl RepoIndex {
|
|||||||
"cargo" => &self.cargo,
|
"cargo" => &self.cargo,
|
||||||
"pypi" => &self.pypi,
|
"pypi" => &self.pypi,
|
||||||
"go" => &self.go,
|
"go" => &self.go,
|
||||||
|
"raw" => &self.raw,
|
||||||
_ => return Arc::new(Vec::new()),
|
_ => return Arc::new(Vec::new()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,6 +141,7 @@ impl RepoIndex {
|
|||||||
"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,
|
"go" => build_go_index(storage).await,
|
||||||
|
"raw" => build_raw_index(storage).await,
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
info!(registry = registry, count = data.len(), "Index rebuilt");
|
info!(registry = registry, count = data.len(), "Index rebuilt");
|
||||||
@@ -147,7 +152,7 @@ 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, usize) {
|
pub fn counts(&self) -> (usize, usize, usize, usize, usize, usize, usize) {
|
||||||
(
|
(
|
||||||
self.docker.count(),
|
self.docker.count(),
|
||||||
self.maven.count(),
|
self.maven.count(),
|
||||||
@@ -155,6 +160,7 @@ impl RepoIndex {
|
|||||||
self.cargo.count(),
|
self.cargo.count(),
|
||||||
self.pypi.count(),
|
self.pypi.count(),
|
||||||
self.go.count(),
|
self.go.count(),
|
||||||
|
self.raw.count(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,6 +370,28 @@ async fn build_go_index(storage: &Storage) -> Vec<RepoInfo> {
|
|||||||
to_sorted_vec(modules)
|
to_sorted_vec(modules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn build_raw_index(storage: &Storage) -> Vec<RepoInfo> {
|
||||||
|
let keys = storage.list("raw/").await;
|
||||||
|
let mut files: HashMap<String, (usize, u64, u64)> = HashMap::new();
|
||||||
|
|
||||||
|
for key in &keys {
|
||||||
|
if let Some(rest) = key.strip_prefix("raw/") {
|
||||||
|
// Group by top-level directory
|
||||||
|
let group = rest.split('/').next().unwrap_or(rest).to_string();
|
||||||
|
let entry = files.entry(group).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(files)
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -517,8 +545,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, g) = idx.counts();
|
let (d, m, n, c, p, g, r) = idx.counts();
|
||||||
assert_eq!((d, m, n, c, p, g), (0, 0, 0, 0, 0, 0));
|
assert_eq!((d, m, n, c, p, g, r), (0, 0, 0, 0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -530,14 +558,15 @@ mod tests {
|
|||||||
idx.invalidate("npm");
|
idx.invalidate("npm");
|
||||||
idx.invalidate("cargo");
|
idx.invalidate("cargo");
|
||||||
idx.invalidate("pypi");
|
idx.invalidate("pypi");
|
||||||
|
idx.invalidate("raw");
|
||||||
idx.invalidate("unknown"); // should be a no-op
|
idx.invalidate("unknown"); // should be a no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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, g) = idx.counts();
|
let (d, m, n, c, p, g, r) = idx.counts();
|
||||||
assert_eq!((d, m, n, c, p, g), (0, 0, 0, 0, 0, 0));
|
assert_eq!((d, m, n, c, p, g, r), (0, 0, 0, 0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub struct RegistryStats {
|
|||||||
pub cargo: usize,
|
pub cargo: usize,
|
||||||
pub pypi: usize,
|
pub pypi: usize,
|
||||||
pub go: usize,
|
pub go: usize,
|
||||||
|
pub raw: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -116,8 +117,9 @@ pub async fn api_stats(State(state): State<Arc<AppState>>) -> Json<RegistryStats
|
|||||||
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 _ = state.repo_index.get("go", &state.storage).await;
|
||||||
|
let _ = state.repo_index.get("raw", &state.storage).await;
|
||||||
|
|
||||||
let (docker, maven, npm, cargo, pypi, go) = state.repo_index.counts();
|
let (docker, maven, npm, cargo, pypi, go, raw) = state.repo_index.counts();
|
||||||
Json(RegistryStats {
|
Json(RegistryStats {
|
||||||
docker,
|
docker,
|
||||||
maven,
|
maven,
|
||||||
@@ -125,6 +127,7 @@ pub async fn api_stats(State(state): State<Arc<AppState>>) -> Json<RegistryStats
|
|||||||
cargo,
|
cargo,
|
||||||
pypi,
|
pypi,
|
||||||
go,
|
go,
|
||||||
|
raw,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +139,7 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
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;
|
let go_repos = state.repo_index.get("go", &state.storage).await;
|
||||||
|
let raw_repos = state.repo_index.get("raw", &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();
|
||||||
@@ -144,7 +148,9 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
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 go_size: u64 = go_repos.iter().map(|r| r.size).sum();
|
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;
|
let raw_size: u64 = raw_repos.iter().map(|r| r.size).sum();
|
||||||
|
let total_storage =
|
||||||
|
docker_size + maven_size + npm_size + cargo_size + pypi_size + go_size + raw_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();
|
||||||
@@ -153,12 +159,14 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
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 go_versions: usize = go_repos.iter().map(|r| r.versions).sum();
|
let go_versions: usize = go_repos.iter().map(|r| r.versions).sum();
|
||||||
|
let raw_versions: usize = raw_repos.iter().map(|r| r.versions).sum();
|
||||||
let total_artifacts = docker_versions
|
let total_artifacts = docker_versions
|
||||||
+ maven_versions
|
+ maven_versions
|
||||||
+ npm_versions
|
+ npm_versions
|
||||||
+ cargo_versions
|
+ cargo_versions
|
||||||
+ pypi_versions
|
+ pypi_versions
|
||||||
+ go_versions;
|
+ go_versions
|
||||||
|
+ raw_versions;
|
||||||
|
|
||||||
let global_stats = GlobalStats {
|
let global_stats = GlobalStats {
|
||||||
downloads: state.metrics.downloads.load(Ordering::Relaxed),
|
downloads: state.metrics.downloads.load(Ordering::Relaxed),
|
||||||
@@ -211,6 +219,13 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
uploads: 0,
|
uploads: 0,
|
||||||
size_bytes: go_size,
|
size_bytes: go_size,
|
||||||
},
|
},
|
||||||
|
RegistryCardStats {
|
||||||
|
name: "raw".to_string(),
|
||||||
|
artifact_count: raw_versions,
|
||||||
|
downloads: state.metrics.get_registry_downloads("raw"),
|
||||||
|
uploads: state.metrics.get_registry_uploads("raw"),
|
||||||
|
size_bytes: raw_size,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let mount_points = vec![
|
let mount_points = vec![
|
||||||
@@ -249,6 +264,11 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
mount_path: "/go/".to_string(),
|
mount_path: "/go/".to_string(),
|
||||||
proxy_upstream: state.config.go.proxy.clone(),
|
proxy_upstream: state.config.go.proxy.clone(),
|
||||||
},
|
},
|
||||||
|
MountPoint {
|
||||||
|
registry: "Raw".to_string(),
|
||||||
|
mount_path: "/raw/".to_string(),
|
||||||
|
proxy_upstream: None,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let activity = state.activity.recent(20);
|
let activity = state.activity.recent(20);
|
||||||
@@ -406,6 +426,13 @@ pub async fn get_registry_stats(storage: &Storage) -> RegistryStats {
|
|||||||
.collect::<HashSet<_>>()
|
.collect::<HashSet<_>>()
|
||||||
.len();
|
.len();
|
||||||
|
|
||||||
|
let raw = all_keys
|
||||||
|
.iter()
|
||||||
|
.filter(|k| k.starts_with("raw/"))
|
||||||
|
.filter_map(|k| k.strip_prefix("raw/")?.split('/').next())
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.len();
|
||||||
|
|
||||||
RegistryStats {
|
RegistryStats {
|
||||||
docker,
|
docker,
|
||||||
maven,
|
maven,
|
||||||
@@ -413,6 +440,7 @@ pub async fn get_registry_stats(storage: &Storage) -> RegistryStats {
|
|||||||
cargo,
|
cargo,
|
||||||
pypi,
|
pypi,
|
||||||
go,
|
go,
|
||||||
|
raw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,3 +985,26 @@ fn extract_pypi_version(name: &str, filename: &str) -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_raw_detail(storage: &Storage, group: &str) -> PackageDetail {
|
||||||
|
let prefix = format!("raw/{}/", group);
|
||||||
|
let keys = storage.list(&prefix).await;
|
||||||
|
|
||||||
|
let mut versions = Vec::new();
|
||||||
|
for key in &keys {
|
||||||
|
if let Some(filename) = key.strip_prefix(&prefix) {
|
||||||
|
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: filename.to_string(),
|
||||||
|
size,
|
||||||
|
published,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PackageDetail { versions }
|
||||||
|
}
|
||||||
|
|||||||
@@ -629,6 +629,7 @@ pub mod icons {
|
|||||||
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="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 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 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 RAW: &str = r#"<path fill="currentColor" d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>"#;
|
||||||
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"/>"#;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ pub fn routes() -> Router<Arc<AppState>> {
|
|||||||
.route("/ui/pypi/{name}", get(pypi_detail))
|
.route("/ui/pypi/{name}", get(pypi_detail))
|
||||||
.route("/ui/go", get(go_list))
|
.route("/ui/go", get(go_list))
|
||||||
.route("/ui/go/{*name}", get(go_detail))
|
.route("/ui/go/{*name}", get(go_detail))
|
||||||
|
.route("/ui/raw", get(raw_list))
|
||||||
|
.route("/ui/raw/{*name}", get(raw_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))
|
||||||
@@ -338,3 +340,41 @@ async fn go_detail(
|
|||||||
let detail = get_go_detail(&state.storage, &name).await;
|
let detail = get_go_detail(&state.storage, &name).await;
|
||||||
Html(render_package_detail("go", &name, &detail, lang))
|
Html(render_package_detail("go", &name, &detail, lang))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Raw pages
|
||||||
|
async fn raw_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_files = state.repo_index.get("raw", &state.storage).await;
|
||||||
|
let (files, total) = paginate(&all_files, page, limit);
|
||||||
|
|
||||||
|
Html(render_registry_list_paginated(
|
||||||
|
"raw",
|
||||||
|
"Raw Storage",
|
||||||
|
&files,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
lang,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn raw_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_raw_detail(&state.storage, &name).await;
|
||||||
|
Html(render_package_detail("raw", &name, &detail, lang))
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ pub fn render_dashboard(data: &DashboardResponse, lang: Lang) -> String {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-6">
|
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-3 mb-6">
|
||||||
{}
|
{}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -656,6 +656,7 @@ pub fn render_package_detail(
|
|||||||
name
|
name
|
||||||
),
|
),
|
||||||
"go" => format!("GOPROXY=http://127.0.0.1:4000/go go get {}", name),
|
"go" => format!("GOPROXY=http://127.0.0.1:4000/go go get {}", name),
|
||||||
|
"raw" => format!("curl -O http://127.0.0.1:4000/raw/{}/<file>", name),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -823,6 +824,7 @@ fn get_registry_icon(registry_type: &str) -> &'static str {
|
|||||||
"cargo" => icons::CARGO,
|
"cargo" => icons::CARGO,
|
||||||
"pypi" => icons::PYPI,
|
"pypi" => icons::PYPI,
|
||||||
"go" => icons::GO,
|
"go" => icons::GO,
|
||||||
|
"raw" => icons::RAW,
|
||||||
_ => {
|
_ => {
|
||||||
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"/>"#
|
||||||
}
|
}
|
||||||
@@ -837,6 +839,7 @@ fn get_registry_title(registry_type: &str) -> &'static str {
|
|||||||
"cargo" => "Cargo Registry",
|
"cargo" => "Cargo Registry",
|
||||||
"pypi" => "PyPI Repository",
|
"pypi" => "PyPI Repository",
|
||||||
"go" => "Go Modules",
|
"go" => "Go Modules",
|
||||||
|
"raw" => "Raw Storage",
|
||||||
_ => "Registry",
|
_ => "Registry",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user