src/address.rs
-rw-r--r-- 6.5 KiB
1use std::{
2 fmt::Display,
3 path::{Path, PathBuf},
4};
5
6use oci_spec::image::Digest;
7use relative_path::RelativePath;
8use uuid::Uuid;
9
10use crate::Namespace;
11
12const SEPARATOR: &str = "/";
13
14fn digest_prefix(digest: &Digest) -> &str {
15 match digest.algorithm() {
16 oci_spec::image::DigestAlgorithm::Sha256 => "sha256",
17 oci_spec::image::DigestAlgorithm::Sha384 => "sha384",
18 oci_spec::image::DigestAlgorithm::Sha512 => "sha512",
19 oci_spec::image::DigestAlgorithm::Other(_) => todo!(),
20 _ => todo!(),
21 }
22}
23
24pub trait Addressable {
25 fn address(&self) -> Address;
26}
27
28/// Address is a path-like object for addressing OCI objects. The design of
29/// this basically copies the file system layout used by the Docker
30/// distribution registry. https://github.com/distribution/distribution
31#[derive(Debug, Clone)]
32pub struct Address {
33 parts: Vec<String>,
34}
35
36impl Address {
37 pub fn as_path(&self, base_dir: &Path) -> PathBuf {
38 let parts = self.parts.join(SEPARATOR);
39 RelativePath::new(&parts).to_path(base_dir)
40 }
41
42 pub fn is_link(&self) -> bool {
43 self.parts.last().is_some_and(|part| part == "link")
44 }
45
46 pub fn is_data(&self) -> bool {
47 self.parts.last().is_some_and(|part| part == "data")
48 }
49
50 pub fn name(&self) -> String {
51 self.parts.last().cloned().unwrap()
52 }
53
54 // /// Create an addressable link
55 // pub fn link(addr: &impl Addressable) -> Self {
56 // let mut parts = addr.parts();
57 // parts.push(String::from("link"));
58 // Address { parts }
59 // }
60
61 // /// create an addressable data path
62 // pub fn data(addr: &impl Addressable) -> Self {
63 // let mut parts = addr.parts();
64 // parts.push(String::from("data"));
65 // Address { parts }
66 // }
67}
68
69impl Display for Address {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.write_str(&self.parts.join(SEPARATOR))
72 }
73}
74
75impl Addressable for Address {
76 fn address(&self) -> Address {
77 self.clone()
78 }
79}
80
81impl From<&Vec<&str>> for Address {
82 fn from(value: &Vec<&str>) -> Self {
83 Address {
84 parts: value.iter().map(|part| part.to_string()).collect(),
85 }
86 }
87}
88
89/// The directory that contains all tags for a certian namespace
90pub struct TagDirectory<'a> {
91 pub namespace: &'a Namespace,
92}
93
94impl<'a> From<&'a Namespace> for TagDirectory<'a> {
95 fn from(value: &'a Namespace) -> Self {
96 TagDirectory { namespace: value }
97 }
98}
99
100impl Addressable for TagDirectory<'_> {
101 fn address(&self) -> Address {
102 (&vec![&self.namespace.as_ref(), "tags"]).into()
103 }
104}
105
106/// Path to a tag within a repository namespace
107pub struct Tag<'a> {
108 pub namespace: &'a Namespace,
109 pub name: &'a str,
110}
111
112impl Addressable for Tag<'_> {
113 fn address(&self) -> Address {
114 (&vec![
115 "repositories",
116 &self.namespace.as_ref(),
117 "tags",
118 self.name,
119 "current",
120 "link",
121 ])
122 .into()
123 }
124}
125
126pub struct Reference<'a> {
127 pub namespace: &'a Namespace,
128 pub digest: &'a Digest,
129}
130
131impl Addressable for Reference<'_> {
132 fn address(&self) -> Address {
133 (&vec![
134 "repositories",
135 &self.namespace.as_ref(),
136 "manifests",
137 "revisions",
138 digest_prefix(self.digest),
139 self.digest.digest(),
140 "link",
141 ])
142 .into()
143 }
144}
145
146/// Path to a temporary blob used during uploads
147pub struct TempBlob<'a> {
148 pub uuid: &'a Uuid,
149 pub namespace: &'a Namespace,
150}
151
152impl Addressable for TempBlob<'_> {
153 fn address(&self) -> Address {
154 (&vec!["repositories", &self.namespace.as_ref(), "tmp", &self.uuid.to_string()]).into()
155 }
156}
157
158/// Path to a blob file on disk
159pub struct Blob<'a> {
160 pub digest: &'a Digest,
161}
162
163impl<'a> From<&'a Digest> for Blob<'a> {
164 fn from(value: &'a Digest) -> Self {
165 Blob { digest: value }
166 }
167}
168
169impl Addressable for Blob<'_> {
170 fn address(&self) -> Address {
171 let digest_str = self.digest.digest();
172 let first_two: String = digest_str.chars().take(2).collect();
173 (&vec!["blobs", digest_prefix(self.digest), &first_two, digest_str]).into()
174 }
175}
176
177/// ManifestRevision is a link to a blob on disk
178pub struct ManifestRevision<'a> {
179 pub namespace: &'a Namespace,
180 pub digest: &'a Digest,
181}
182
183impl Addressable for ManifestRevision<'_> {
184 fn address(&self) -> Address {
185 let digest_str = self.digest.digest();
186 (&vec![
187 "repositories",
188 &self.namespace.as_ref(),
189 "manifests",
190 "revisions",
191 digest_prefix(self.digest),
192 &digest_str,
193 "link",
194 ])
195 .into()
196 }
197}
198
199pub struct LayerLink<'a> {
200 pub namespace: &'a Namespace,
201 pub digest: &'a Digest,
202}
203
204impl Addressable for LayerLink<'_> {
205 fn address(&self) -> Address {
206 let digest_str = self.digest.digest();
207 (&vec![
208 "repositories",
209 self.namespace.as_ref(),
210 "layers",
211 digest_prefix(self.digest),
212 &digest_str,
213 "link",
214 ])
215 .into()
216 }
217}
218
219#[cfg(test)]
220mod test {
221 use std::str::FromStr;
222
223 use super::*;
224
225 #[test]
226 pub fn addresses() {
227 let namespace = Namespace::from_str("hello/world").unwrap();
228 assert!(&TagDirectory::from(&namespace).address().to_string() == "hello/world/tags");
229 assert!(
230 &Tag {
231 namespace: &namespace,
232 name: "latest"
233 }
234 .address()
235 .to_string()
236 == "repositories/hello/world/tags/latest/current/link"
237 );
238 let uuid = Uuid::new_v4();
239 assert!(TempBlob{uuid: &uuid, namespace: &namespace}.address().to_string() == format!("repositories/hello/world/tmp/{}", uuid));
240 let digest = Digest::from_str(
241 "sha256:57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f",
242 )
243 .unwrap();
244 assert!(
245 Blob { digest: &digest }.address().to_string()
246 == "blobs/sha256/57/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f"
247 );
248 assert!(
249 ManifestRevision {
250 namespace: &namespace,
251 digest: &digest
252 }
253 .address()
254 .to_string()
255 == "repositories/hello/world/manifests/revisions/sha256/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f/link"
256 )
257 }
258}