holger-server-lib 0.6.6

Holger server library: config, wiring, gRPC service, Rust API
use std::sync::Arc;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use repository_file_rust::RustRepoFile as RustRepo;
use repository_znippy_rust::RustRepoZnippy;
use repository_znippy_maven::MavenRepoZnippy;
use repository_znippy_python::PipRepoZnippy;
use repository_znippy_npm::NpmRepoZnippy;
use repository_znippy_nuget::NugetRepoZnippy;
use repository_znippy_go::GoRepoZnippy;
use repository_znippy_gem::GemRepoZnippy;
use repository_znippy_deb::DebRepoZnippy;
use repository_znippy_rpm::RpmRepoZnippy;
use repository_znippy_helm::HelmRepoZnippy;
use repository_file_oci::OciRepoFile;
use repository_znippy_docker::DockerRepoZnippy;
use repository_znippy_conda::CondaRepoZnippy;
use repository_znippy_composer::ComposerRepoZnippy;
use repository_znippy_package::ZnippyRepoZnippy;
use traits::RepositoryBackendTrait;
use crate::upstream_rust::RustRegistryUpstream;
use crate::{ExposedEndpoint, StorageEndpoint};

#[derive(Serialize, Deserialize)]
pub struct Repository {
    // Parsed from RON
    pub ron_name: String,
    pub ron_repo_type: String,        // rust/java/python/raw
    pub ron_upstreams: Vec<String>,   // empty means no upstreams
    pub ron_in: Option<RepositoryIO>,
    pub ron_out: Option<RepositoryIO>,

    /// Optional path to a znippy archive (.znippy file)
    #[serde(default)]
    pub ron_archive_path: Option<String>,

    /// Optional storage directory for file-backed (writable) repos.
    /// Defaults to /var/lib/holger/{ron_name} when omitted.
    #[serde(default)]
    pub ron_data_dir: Option<String>,

    /// Public base URL used in config.json `dl` for the disk-backed Rust repo.
    /// E.g. `https://holger.example.com`.  Defaults to `https://127.0.0.1:8443`
    /// when omitted.
    #[serde(default)]
    pub ron_base_url: Option<String>,

    /// Optional path to a WRITABLE znippy archive (rust repos). When set, the repo
    /// is a live `/sparring`-style registry: crates are appended into this single
    /// `.znippy` (created on first push, reboot-safe re-seal per write) and can
    /// later be `seal`ed into a static "drift" snapshot. Distinct from
    /// `ron_archive_path` (read-only drift) and `ron_data_dir` (loose-file repo).
    #[serde(default)]
    pub ron_writable_archive: Option<String>,

    // Wired in second pass
    #[serde(skip_serializing, skip_deserializing, default)]
    pub backend_repository: Option<Arc<dyn RepositoryBackendTrait>>,
    


    #[serde(skip_serializing, skip_deserializing, default)]
    pub wired_upstreams: Vec<*const Repository>, // or &Repository pinned after build
}
impl Repository {
    pub fn backend_from_config(&mut self) -> anyhow::Result<()> {
        // Build a znippy-backed repo that requires an archive path.
        macro_rules! znippy_backed {
            ($ty:ty, $label:expr) => {{
                if let Some(archive_path) = &self.ron_archive_path {
                    let repo = <$ty>::with_archive(
                        self.ron_name.clone(),
                        PathBuf::from(archive_path),
                    )?;
                    self.backend_repository = Some(Arc::new(repo));
                } else {
                    anyhow::bail!(concat!($label, " repository requires ron_archive_path to a znippy archive"));
                }
                Ok(())
            }};
        }

        match self.ron_repo_type.as_str() {
            "rust" => {
                if let Some(writable) = &self.ron_writable_archive {
                    // Writable znippy-archive registry (the /sparring + /cache
                    // primary): seal-able into a drift snapshot, reboot-safe.
                    let mut repo = RustRepoZnippy::with_writable_archive(
                        self.ron_name.clone(),
                        PathBuf::from(writable),
                    );
                    if let Some(url) = &self.ron_base_url {
                        repo = repo.with_base_url(url.clone());
                    }
                    self.backend_repository = Some(Arc::new(repo));
                } else if let Some(archive_path) = &self.ron_archive_path {
                    let base_url = self
                        .ron_base_url
                        .clone()
                        .unwrap_or_else(|| "https://127.0.0.1:8443".to_string());
                    let repo = RustRepoZnippy::with_archive_and_url(
                        self.ron_name.clone(),
                        PathBuf::from(archive_path),
                        base_url,
                    )?;
                    self.backend_repository = Some(Arc::new(repo));
                } else {
                    // Writable, disk-backed dev backend. Requires a storage dir.
                    let dir = self.ron_data_dir.clone().ok_or_else(|| {
                        anyhow::anyhow!(
                            "Rust file-backed repository requires ron_data_dir (storage directory)"
                        )
                    })?;
                    let base_url = self
                        .ron_base_url
                        .clone()
                        .unwrap_or_else(|| "https://127.0.0.1:8443".to_string());
                    let repo = RustRepo::new(self.ron_name.clone(), PathBuf::from(dir), base_url);
                    self.backend_repository = Some(Arc::new(repo));
                }
                Ok(())
            }
            // Read-only REMOTE rust registry (the /cache upstream). Defaults to
            // crates.io; `ron_base_url` (when set) overrides the sparse index base.
            // Pair with a writable "rust" + `ron_upstreams` to get a caching mirror.
            "rust-remote" => {
                let repo = match &self.ron_base_url {
                    Some(idx) => RustRegistryUpstream::new(
                        self.ron_name.clone(),
                        idx.clone(),
                        format!("{}/crates", idx.trim_end_matches('/')),
                    ),
                    None => RustRegistryUpstream::crates_io(self.ron_name.clone()),
                };
                self.backend_repository = Some(Arc::new(repo));
                Ok(())
            }
            "maven3" => znippy_backed!(MavenRepoZnippy, "Maven3"),
            "pip" => znippy_backed!(PipRepoZnippy, "Pip"),
            "npm" => znippy_backed!(NpmRepoZnippy, "Npm"),
            "nuget" => znippy_backed!(NugetRepoZnippy, "Nuget"),
            "go" => znippy_backed!(GoRepoZnippy, "Go"),
            "gem" => znippy_backed!(GemRepoZnippy, "Gem"),
            "deb" => znippy_backed!(DebRepoZnippy, "Deb"),
            "rpm" => znippy_backed!(RpmRepoZnippy, "Rpm"),
            "helm" => znippy_backed!(HelmRepoZnippy, "Helm"),
            "docker" => znippy_backed!(DockerRepoZnippy, "Docker"),
            // Writable OCI registry (Distribution Spec v2). Push target for the
            // modern Helm flow (`helm push oci://…`) and any OCI artifact; needs
            // no archive — it owns a disk-backed, content-addressed store.
            "oci" => {
                let dir = self
                    .ron_data_dir
                    .clone()
                    .unwrap_or_else(|| format!("/var/lib/holger/{}", self.ron_name));
                let repo = OciRepoFile::new(self.ron_name.clone(), PathBuf::from(dir))?;
                self.backend_repository = Some(Arc::new(repo));
                Ok(())
            }
            "conda" => znippy_backed!(CondaRepoZnippy, "Conda"),
            "composer" => znippy_backed!(ComposerRepoZnippy, "Composer"),
            "znippy" => znippy_backed!(ZnippyRepoZnippy, "Znippy"),
            other => anyhow::bail!("Unsupported repository type: {}", other),
        }
    }

}




#[derive(Serialize, Deserialize)]
pub struct RepositoryIO {
    pub ron_storage_endpoint: String,
    pub ron_exposed_endpoint: String,


    #[serde(skip_serializing, skip_deserializing, default = "std::ptr::null")]
    pub wired_storage: *const StorageEndpoint,
    #[serde(skip_serializing, skip_deserializing, default = "std::ptr::null")]
    pub wired_exposed: *const ExposedEndpoint,
}

#[cfg(test)]
mod tests {
    use super::*;
    use traits::ArtifactId;

    fn rust_repo(name: &str) -> Repository {
        Repository {
            ron_name: name.to_string(),
            ron_repo_type: "rust".to_string(),
            ron_upstreams: vec![],
            ron_in: None,
            ron_out: None,
            ron_archive_path: None,
            ron_data_dir: None,
            ron_base_url: None,
            ron_writable_archive: None,
            backend_repository: None,
            wired_upstreams: vec![],
        }
    }

    /// `ron_writable_archive` (the `/sparring` + `/cache`-primary config) builds a
    /// WRITABLE znippy-backed rust backend that accepts a crate and serves it back.
    #[test]
    fn writable_archive_config_builds_writable_backend() {
        let dir = tempfile::tempdir().unwrap();
        let archive = dir.path().join("sparring.znippy");

        let mut repo = rust_repo("sparring");
        repo.ron_writable_archive = Some(archive.to_string_lossy().into_owned());
        repo.backend_from_config().unwrap();

        let backend = repo.backend_repository.as_ref().expect("backend built");
        assert!(backend.is_writable(), "ron_writable_archive must yield a writable backend");

        let id = ArtifactId { namespace: None, name: "serde".into(), version: "1.0.0".into() };
        backend.put(&id, b"crate-bytes").unwrap();
        assert_eq!(
            backend.fetch(&id).unwrap().as_deref(),
            Some(b"crate-bytes".as_slice()),
            "writable backend must serve the crate it was given"
        );
    }

    /// `ron_archive_path` stays the read-only "drift" backend: seal a sparring
    /// archive (via the writable path), reopen it read-only, confirm not writable
    /// but still serves the sealed crate.
    #[test]
    fn archive_path_config_is_read_only_drift() {
        let dir = tempfile::tempdir().unwrap();
        let archive = dir.path().join("drift.znippy");
        let id = ArtifactId { namespace: None, name: "x".into(), version: "1.0.0".into() };

        let mut seed = rust_repo("seed");
        seed.ron_writable_archive = Some(archive.to_string_lossy().into_owned());
        seed.backend_from_config().unwrap();
        seed.backend_repository.unwrap().put(&id, b"sealed-bytes").unwrap();

        let mut ro = rust_repo("drift");
        ro.ron_archive_path = Some(archive.to_string_lossy().into_owned());
        ro.backend_from_config().unwrap();
        let backend = ro.backend_repository.unwrap();
        assert!(!backend.is_writable(), "ron_archive_path must yield a read-only backend");
        assert_eq!(
            backend.fetch(&id).unwrap().as_deref(),
            Some(b"sealed-bytes".as_slice()),
            "read-only drift backend must serve the sealed crate"
        );
    }

    /// `rust-remote` builds a read-only remote-registry upstream (the /cache
    /// upstream); crates.io by default, no archive/dir required.
    #[test]
    fn rust_remote_config_builds_read_only_upstream() {
        let mut repo = rust_repo("crates-io");
        repo.ron_repo_type = "rust-remote".to_string();
        repo.backend_from_config().unwrap();
        let backend = repo.backend_repository.as_ref().expect("backend built");
        assert!(!backend.is_writable(), "remote upstream must be read-only");
        assert_eq!(backend.name(), "crates-io");
    }
}