1 | use std::{ |
2 | fmt::Display, |
3 | path::{Path, PathBuf}, |
4 | }; |
5 | |
6 | use oci_spec::image::Digest; |
7 | use relative_path::RelativePath; |
8 | use uuid::Uuid; |
9 | |
10 | use crate::Namespace; |
11 | |
12 | const SEPARATOR: &str = "/"; |
13 | |
14 | fn 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 | |
24 | pub 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)] |
32 | pub struct Address { |
33 | parts: Vec<String>, |
34 | } |
35 | |
36 | impl 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 | |
69 | impl 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 | |
75 | impl Addressable for Address { |
76 | fn address(&self) -> Address { |
77 | self.clone() |
78 | } |
79 | } |
80 | |
81 | impl 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 |
90 | pub struct TagDirectory<'a> { |
91 | pub namespace: &'a Namespace, |
92 | } |
93 | |
94 | impl<'a> From<&'a Namespace> for TagDirectory<'a> { |
95 | fn from(value: &'a Namespace) -> Self { |
96 | TagDirectory { namespace: value } |
97 | } |
98 | } |
99 | |
100 | impl 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 |
107 | pub struct Tag<'a> { |
108 | pub namespace: &'a Namespace, |
109 | pub name: &'a str, |
110 | } |
111 | |
112 | impl 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 | |
126 | pub struct Reference<'a> { |
127 | pub namespace: &'a Namespace, |
128 | pub digest: &'a Digest, |
129 | } |
130 | |
131 | impl 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 |
147 | pub struct TempBlob<'a> { |
148 | pub uuid: &'a Uuid, |
149 | pub namespace: &'a Namespace, |
150 | } |
151 | |
152 | impl 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 |
159 | pub struct Blob<'a> { |
160 | pub digest: &'a Digest, |
161 | } |
162 | |
163 | impl<'a> From<&'a Digest> for Blob<'a> { |
164 | fn from(value: &'a Digest) -> Self { |
165 | Blob { digest: value } |
166 | } |
167 | } |
168 | |
169 | impl 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 |
178 | pub struct ManifestRevision<'a> { |
179 | pub namespace: &'a Namespace, |
180 | pub digest: &'a Digest, |
181 | } |
182 | |
183 | impl 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 | |
199 | pub struct LayerLink<'a> { |
200 | pub namespace: &'a Namespace, |
201 | pub digest: &'a Digest, |
202 | } |
203 | |
204 | impl 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)] |
220 | mod 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 | } |