use std::sync::Arc;
use skidbladnir::federation::{
ArtifactCoord, ArtifactoryRegistry, AwsCodeArtifactRegistry, AzureArtifactsRegistry,
NexusRegistry, RegistryFormat, UpstreamRegistry,
};
use traits::{
ArtifactFormat, ArtifactId, ArtifactMeta, RemoteUpstream, RepositoryBackendTrait, UpstreamAuth,
};
fn to_registry_format(fmt: &ArtifactFormat) -> RegistryFormat {
match fmt {
ArtifactFormat::Rust => RegistryFormat::Rust,
ArtifactFormat::Maven3 => RegistryFormat::Maven,
ArtifactFormat::Pip => RegistryFormat::Pip,
ArtifactFormat::Npm => RegistryFormat::Npm,
ArtifactFormat::Nuget => RegistryFormat::Nuget,
ArtifactFormat::Gem => RegistryFormat::Gem,
_ => RegistryFormat::Raw,
}
}
fn to_registry_auth(auth: &UpstreamAuth) -> skidbladnir::federation::UpstreamAuth {
use skidbladnir::federation::UpstreamAuth as SUA;
match auth {
UpstreamAuth::None => SUA::None,
UpstreamAuth::Basic { username, password } => {
SUA::Basic { username: username.clone(), password: password.clone() }
}
UpstreamAuth::Bearer { token } => SUA::Bearer { token: token.clone() },
UpstreamAuth::ApiKey { header, key } => {
SUA::ApiKey { header: header.clone(), key: key.clone() }
}
UpstreamAuth::Mtls { cert_pem, key_pem } => {
SUA::Mtls { cert_pem: cert_pem.clone(), key_pem: key_pem.clone() }
}
}
}
fn to_coord(id: &ArtifactId) -> ArtifactCoord {
ArtifactCoord {
namespace: id.namespace.clone(),
name: id.name.clone(),
version: id.version.clone(),
}
}
pub struct SkidbladnirUpstream {
inner: Box<dyn UpstreamRegistry>,
format: ArtifactFormat,
auth: UpstreamAuth,
}
impl SkidbladnirUpstream {
fn new(inner: Box<dyn UpstreamRegistry>, format: ArtifactFormat, auth: UpstreamAuth) -> Self {
Self { inner, format, auth }
}
pub fn artifactory(
name: impl Into<String>,
base_url: impl Into<String>,
repo: impl Into<String>,
auth: UpstreamAuth,
format: ArtifactFormat,
) -> Self {
let inner = ArtifactoryRegistry::new(
name,
base_url,
repo,
to_registry_auth(&auth),
to_registry_format(&format),
);
Self::new(Box::new(inner), format, auth)
}
pub fn nexus(
name: impl Into<String>,
base_url: impl Into<String>,
repo: impl Into<String>,
auth: UpstreamAuth,
format: ArtifactFormat,
) -> Self {
let inner = NexusRegistry::new(
name,
base_url,
repo,
to_registry_auth(&auth),
to_registry_format(&format),
);
Self::new(Box::new(inner), format, auth)
}
pub fn azure(
name: impl Into<String>,
organization: impl Into<String>,
repo: impl Into<String>,
auth: UpstreamAuth,
format: ArtifactFormat,
) -> Self {
let repo = repo.into();
let (project, feed) = match repo.split_once('/') {
Some((p, f)) => (Some(p.to_string()), f.to_string()),
None => (None, repo),
};
let inner = AzureArtifactsRegistry::new(
name,
organization,
project,
feed,
to_registry_auth(&auth),
to_registry_format(&format),
);
Self::new(Box::new(inner), format, auth)
}
pub fn aws(
name: impl Into<String>,
endpoint: impl Into<String>,
auth: UpstreamAuth,
format: ArtifactFormat,
) -> Self {
let token = match &auth {
UpstreamAuth::Bearer { token } => token.clone(),
UpstreamAuth::Basic { password, .. } => password.clone(),
_ => String::new(),
};
let inner = AwsCodeArtifactRegistry::new(name, endpoint, token, to_registry_format(&format));
Self::new(Box::new(inner), format, auth)
}
pub fn mirror_into(
&self,
ids: &[ArtifactId],
sink: &mut dyn FnMut(&ArtifactId, Vec<u8>) -> anyhow::Result<()>,
) -> anyhow::Result<usize> {
let coords: Vec<ArtifactCoord> = ids.iter().map(to_coord).collect();
let mut mirrored = 0usize;
for (id, coord) in ids.iter().zip(coords.iter()) {
if let Some(bytes) = self.inner.fetch(coord)? {
sink(id, bytes)?;
mirrored += 1;
}
}
Ok(mirrored)
}
}
impl RepositoryBackendTrait for SkidbladnirUpstream {
fn name(&self) -> &str {
self.inner.name()
}
fn format(&self) -> ArtifactFormat {
self.format.clone()
}
fn is_writable(&self) -> bool {
false
}
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
self.inner.fetch(&to_coord(id))
}
fn put(&self, _id: &ArtifactId, _data: &[u8]) -> anyhow::Result<()> {
anyhow::bail!("{}: remote upstream is read-only", self.inner.name())
}
fn handle_http2_request(
&self,
_method: &str,
suburl: &str,
_body: &[u8],
) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
let parts: Vec<&str> = suburl.trim_start_matches('/').split('/').collect();
match parts.as_slice() {
[_repo, "crates", name, version, "download"] => {
let id = ArtifactId {
namespace: None,
name: (*name).to_string(),
version: (*version).to_string(),
};
match self.fetch(&id)? {
Some(body) => Ok((
200,
vec![("Content-Type".into(), "application/octet-stream".into())],
body,
)),
None => Ok((404, Vec::new(), b"Not found upstream".to_vec())),
}
}
_ => Ok((404, Vec::new(), b"Not found".to_vec())),
}
}
}
impl RemoteUpstream for SkidbladnirUpstream {
fn base_url(&self) -> &str {
self.inner.base_url()
}
fn auth(&self) -> &UpstreamAuth {
&self.auth
}
fn metadata(&self, id: &ArtifactId) -> anyhow::Result<Option<ArtifactMeta>> {
Ok(self.inner.metadata(&to_coord(id))?.map(|m| ArtifactMeta {
size: m.size,
sha256: m.sha256,
content_type: m.content_type,
}))
}
}
pub fn as_backend(up: SkidbladnirUpstream) -> Arc<dyn RepositoryBackendTrait> {
Arc::new(up)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_translation_is_field_preserving() {
use skidbladnir::federation::UpstreamAuth as SUA;
let cases = [
UpstreamAuth::None,
UpstreamAuth::Basic { username: "u".into(), password: "p".into() },
UpstreamAuth::Bearer { token: "t".into() },
UpstreamAuth::ApiKey { header: "X-Api".into(), key: "k".into() },
];
for c in cases {
let mapped = to_registry_auth(&c);
match (&c, &mapped) {
(UpstreamAuth::None, SUA::None) => {}
(UpstreamAuth::Basic { username, password }, SUA::Basic { username: u2, password: p2 }) => {
assert_eq!((username, password), (u2, p2));
}
(UpstreamAuth::Bearer { token }, SUA::Bearer { token: t2 }) => assert_eq!(token, t2),
(UpstreamAuth::ApiKey { header, key }, SUA::ApiKey { header: h2, key: k2 }) => {
assert_eq!((header, key), (h2, k2));
}
_ => panic!("auth variant mistranslated"),
}
}
}
#[test]
fn format_translation_maps_and_falls_back() {
assert_eq!(to_registry_format(&ArtifactFormat::Rust), RegistryFormat::Rust);
assert_eq!(to_registry_format(&ArtifactFormat::Maven3), RegistryFormat::Maven);
assert_eq!(to_registry_format(&ArtifactFormat::Docker), RegistryFormat::Raw);
}
#[test]
fn adapter_is_read_only_and_delegates_identity() {
let up = SkidbladnirUpstream::nexus(
"nx", "https://nexus.corp", "crates-proxy", UpstreamAuth::None, ArtifactFormat::Rust,
);
assert_eq!(RepositoryBackendTrait::name(&up), "nx");
assert!(!up.is_writable());
assert_eq!(up.base_url(), "https://nexus.corp");
let id = ArtifactId { namespace: None, name: "x".into(), version: "1".into() };
assert!(up.put(&id, b"d").is_err(), "remote upstream must reject writes");
}
#[test]
fn azure_repo_splits_project_and_feed() {
let scoped = SkidbladnirUpstream::azure(
"az", "contoso", "proj/myfeed", UpstreamAuth::None, ArtifactFormat::Npm,
);
assert_eq!(scoped.base_url(), "https://pkgs.dev.azure.com/contoso");
}
}