use std::{ fmt::Display, path::{Path, PathBuf}, }; use oci_spec::image::Digest; use relative_path::RelativePath; use uuid::Uuid; use crate::Namespace; const SEPARATOR: &str = "/"; fn digest_prefix(digest: &Digest) -> &str { match digest.algorithm() { oci_spec::image::DigestAlgorithm::Sha256 => "sha256", oci_spec::image::DigestAlgorithm::Sha384 => "sha384", oci_spec::image::DigestAlgorithm::Sha512 => "sha512", oci_spec::image::DigestAlgorithm::Other(_) => todo!(), _ => todo!(), } } pub trait Addressable { fn address(&self) -> Address; } /// Address is a path-like object for addressing OCI objects. The design of /// this basically copies the file system layout used by the Docker /// distribution registry. https://github.com/distribution/distribution #[derive(Debug, Clone)] pub struct Address { parts: Vec, } impl Address { pub fn as_path(&self, base_dir: &Path) -> PathBuf { let parts = self.parts.join(SEPARATOR); RelativePath::new(&parts).to_path(base_dir) } pub fn is_link(&self) -> bool { self.parts.last().is_some_and(|part| part == "link") } pub fn is_data(&self) -> bool { self.parts.last().is_some_and(|part| part == "data") } pub fn name(&self) -> String { self.parts.last().cloned().unwrap() } // /// Create an addressable link // pub fn link(addr: &impl Addressable) -> Self { // let mut parts = addr.parts(); // parts.push(String::from("link")); // Address { parts } // } // /// create an addressable data path // pub fn data(addr: &impl Addressable) -> Self { // let mut parts = addr.parts(); // parts.push(String::from("data")); // Address { parts } // } } impl Display for Address { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.parts.join(SEPARATOR)) } } impl Addressable for Address { fn address(&self) -> Address { self.clone() } } impl From<&Vec<&str>> for Address { fn from(value: &Vec<&str>) -> Self { Address { parts: value.iter().map(|part| part.to_string()).collect(), } } } /// The directory that contains all tags for a certian namespace pub struct TagDirectory<'a> { pub namespace: &'a Namespace, } impl<'a> From<&'a Namespace> for TagDirectory<'a> { fn from(value: &'a Namespace) -> Self { TagDirectory { namespace: value } } } impl Addressable for TagDirectory<'_> { fn address(&self) -> Address { (&vec![&self.namespace.as_ref(), "tags"]).into() } } /// Path to a tag within a repository namespace pub struct Tag<'a> { pub namespace: &'a Namespace, pub name: &'a str, } impl Addressable for Tag<'_> { fn address(&self) -> Address { (&vec![ "repositories", &self.namespace.as_ref(), "tags", self.name, "current", "link", ]) .into() } } pub struct Reference<'a> { pub namespace: &'a Namespace, pub digest: &'a Digest, } impl Addressable for Reference<'_> { fn address(&self) -> Address { (&vec![ "repositories", &self.namespace.as_ref(), "manifests", "revisions", digest_prefix(self.digest), self.digest.digest(), "link", ]) .into() } } /// Path to a temporary blob used during uploads pub struct TempBlob<'a> { pub uuid: &'a Uuid, pub namespace: &'a Namespace, } impl Addressable for TempBlob<'_> { fn address(&self) -> Address { (&vec!["repositories", &self.namespace.as_ref(), "tmp", &self.uuid.to_string()]).into() } } /// Path to a blob file on disk pub struct Blob<'a> { pub digest: &'a Digest, } impl<'a> From<&'a Digest> for Blob<'a> { fn from(value: &'a Digest) -> Self { Blob { digest: value } } } impl Addressable for Blob<'_> { fn address(&self) -> Address { let digest_str = self.digest.digest(); let first_two: String = digest_str.chars().take(2).collect(); (&vec!["blobs", digest_prefix(self.digest), &first_two, digest_str]).into() } } /// ManifestRevision is a link to a blob on disk pub struct ManifestRevision<'a> { pub namespace: &'a Namespace, pub digest: &'a Digest, } impl Addressable for ManifestRevision<'_> { fn address(&self) -> Address { let digest_str = self.digest.digest(); (&vec![ "repositories", &self.namespace.as_ref(), "manifests", "revisions", digest_prefix(self.digest), &digest_str, "link", ]) .into() } } pub struct LayerLink<'a> { pub namespace: &'a Namespace, pub digest: &'a Digest, } impl Addressable for LayerLink<'_> { fn address(&self) -> Address { let digest_str = self.digest.digest(); (&vec![ "repositories", self.namespace.as_ref(), "layers", digest_prefix(self.digest), &digest_str, "link", ]) .into() } } #[cfg(test)] mod test { use std::str::FromStr; use super::*; #[test] pub fn addresses() { let namespace = Namespace::from_str("hello/world").unwrap(); assert!(&TagDirectory::from(&namespace).address().to_string() == "hello/world/tags"); assert!( &Tag { namespace: &namespace, name: "latest" } .address() .to_string() == "repositories/hello/world/tags/latest/current/link" ); let uuid = Uuid::new_v4(); assert!(TempBlob{uuid: &uuid, namespace: &namespace}.address().to_string() == format!("repositories/hello/world/tmp/{}", uuid)); let digest = Digest::from_str( "sha256:57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f", ) .unwrap(); assert!( Blob { digest: &digest }.address().to_string() == "blobs/sha256/57/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f" ); assert!( ManifestRevision { namespace: &namespace, digest: &digest } .address() .to_string() == "repositories/hello/world/manifests/revisions/sha256/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f/link" ) } }