Author:
Hash:
Timestamp:
+204 -6 +/-6 browse
Kevin Schoon [me@kevinschoon.com]
4f996b94f16d284a7cb2ab8f31482ab5ee82bbe4
Fri, 04 Apr 2025 14:44:44 +0000 (4 months ago)
1 | diff --git a/Cargo.toml b/Cargo.toml |
2 | index d47df22..2677521 100644 |
3 | --- a/Cargo.toml |
4 | +++ b/Cargo.toml |
5 | @@ -4,7 +4,7 @@ version = "0.1.0" |
6 | edition = "2024" |
7 | |
8 | [dependencies] |
9 | - axum = { version = "0.8.3", features = ["macros", "query"] } |
10 | + axum = { version = "0.8.3", features = ["macros", "query", "json"] } |
11 | oci-spec = "0.7.1" |
12 | serde = "1.0.219" |
13 | serde_json = "1.0.140" |
14 | diff --git a/src/address.rs b/src/address.rs |
15 | new file mode 100644 |
16 | index 0000000..d8032c9 |
17 | --- /dev/null |
18 | +++ b/src/address.rs |
19 | @@ -0,0 +1,163 @@ |
20 | + use std::{ |
21 | + fmt::Display, |
22 | + path::{Path, PathBuf}, |
23 | + }; |
24 | + |
25 | + use oci_spec::image::Digest; |
26 | + use uuid::Uuid; |
27 | + |
28 | + use crate::Namespace; |
29 | + |
30 | + const SEPARATOR: &str = "/"; |
31 | + |
32 | + /// Address is a path-like object for addressing OCI objects. The design of |
33 | + /// this basically copies the file system layout used by the Docker |
34 | + /// distribution registry. https://github.com/distribution/distribution |
35 | + #[derive(Debug, Clone)] |
36 | + pub struct Address { |
37 | + parts: Vec<String>, |
38 | + } |
39 | + |
40 | + impl Address { |
41 | + pub fn as_path(&self, base_dir: &Path) -> PathBuf { |
42 | + let parts = self.parts.join(SEPARATOR); |
43 | + let path = Path::new(&parts); |
44 | + base_dir.join(path) |
45 | + } |
46 | + |
47 | + pub fn link(&self) -> Self { |
48 | + let mut parts = self.parts.clone(); |
49 | + parts.push(String::from("link")); |
50 | + Address { parts } |
51 | + } |
52 | + |
53 | + pub fn data(&self) -> Self { |
54 | + let mut parts = self.parts.clone(); |
55 | + parts.push(String::from("data")); |
56 | + Address { parts } |
57 | + } |
58 | + } |
59 | + |
60 | + impl Display for Address { |
61 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
62 | + f.write_str(&self.parts.join(SEPARATOR)) |
63 | + } |
64 | + } |
65 | + |
66 | + /// The directory that contains all tags for a certian namespace |
67 | + pub struct TagDirectory<'a> { |
68 | + pub namespace: &'a Namespace, |
69 | + } |
70 | + |
71 | + impl<'a> From<&'a Namespace> for TagDirectory<'a> { |
72 | + fn from(value: &'a Namespace) -> Self { |
73 | + TagDirectory { namespace: value } |
74 | + } |
75 | + } |
76 | + |
77 | + impl From<TagDirectory<'_>> for Address { |
78 | + fn from(value: TagDirectory<'_>) -> Self { |
79 | + Address { |
80 | + parts: vec![value.namespace.to_string(), String::from("tags")], |
81 | + } |
82 | + } |
83 | + } |
84 | + |
85 | + /// Path to a tag within a repository namespace |
86 | + pub struct Tag<'a> { |
87 | + pub namespace: &'a Namespace, |
88 | + pub name: &'a str, |
89 | + } |
90 | + |
91 | + impl From<Tag<'_>> for Address { |
92 | + fn from(value: Tag<'_>) -> Self { |
93 | + Address { |
94 | + parts: vec![ |
95 | + value.namespace.to_string(), |
96 | + String::from("tags"), |
97 | + value.name.to_string(), |
98 | + ], |
99 | + } |
100 | + } |
101 | + } |
102 | + |
103 | + /// Path to a temporary blob used during uploads |
104 | + pub struct TempBlob<'a> { |
105 | + pub uuid: &'a Uuid, |
106 | + } |
107 | + |
108 | + impl<'a> From<&'a Uuid> for TempBlob<'a> { |
109 | + fn from(value: &'a Uuid) -> Self { |
110 | + TempBlob { uuid: value } |
111 | + } |
112 | + } |
113 | + |
114 | + impl From<TempBlob<'_>> for Address { |
115 | + fn from(value: TempBlob<'_>) -> Self { |
116 | + Address { |
117 | + parts: vec![String::from("tmp"), value.uuid.to_string()], |
118 | + } |
119 | + } |
120 | + } |
121 | + |
122 | + /// Path to a blob file on disk |
123 | + pub struct Blob<'a> { |
124 | + pub digest: &'a Digest, |
125 | + } |
126 | + |
127 | + impl<'a> From<&'a Digest> for Blob<'a> { |
128 | + fn from(value: &'a Digest) -> Self { |
129 | + Blob { digest: value } |
130 | + } |
131 | + } |
132 | + |
133 | + impl From<Blob<'_>> for Address { |
134 | + fn from(value: Blob<'_>) -> Self { |
135 | + let digest_str = value.digest.digest(); |
136 | + let first_two: String = digest_str.chars().take(2).collect(); |
137 | + let parts = match value.digest.algorithm() { |
138 | + oci_spec::image::DigestAlgorithm::Sha256 => vec![ |
139 | + String::from("blobs"), |
140 | + String::from("sha256"), |
141 | + first_two, |
142 | + digest_str.to_string(), |
143 | + ], |
144 | + oci_spec::image::DigestAlgorithm::Sha384 => todo!(), |
145 | + oci_spec::image::DigestAlgorithm::Sha512 => todo!(), |
146 | + oci_spec::image::DigestAlgorithm::Other(_) => todo!(), |
147 | + _ => todo!(), |
148 | + }; |
149 | + Address { parts } |
150 | + } |
151 | + } |
152 | + |
153 | + #[cfg(test)] |
154 | + mod test { |
155 | + use std::str::FromStr; |
156 | + |
157 | + use super::*; |
158 | + |
159 | + #[test] |
160 | + pub fn addresses() { |
161 | + let namespace = Namespace::from_str("hello/world").unwrap(); |
162 | + assert!(Address::from(TagDirectory::from(&namespace)).to_string() == "hello/world/tags"); |
163 | + assert!( |
164 | + Address::from(Tag { |
165 | + namespace: &namespace, |
166 | + name: "latest" |
167 | + }) |
168 | + .to_string() |
169 | + == "hello/world/tags/latest" |
170 | + ); |
171 | + let uuid = Uuid::new_v4(); |
172 | + assert!(Address::from(TempBlob::from(&uuid)).to_string() == format!("tmp/{}", uuid)); |
173 | + let digest = Digest::from_str( |
174 | + "sha256:57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f", |
175 | + ) |
176 | + .unwrap(); |
177 | + assert!( |
178 | + Address::from(Blob { digest: &digest }).to_string() |
179 | + == "blobs/sha256/57/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f" |
180 | + ) |
181 | + } |
182 | + } |
183 | diff --git a/src/handlers.rs b/src/handlers.rs |
184 | index e5f29c2..60fbdbe 100644 |
185 | --- a/src/handlers.rs |
186 | +++ b/src/handlers.rs |
187 | @@ -1,13 +1,18 @@ |
188 | use std::str::FromStr; |
189 | |
190 | use axum::{ |
191 | - body::BodyDataStream, extract::{Path, Query, Request, State}, http::StatusCode, response::Response, Extension, Json |
192 | + Extension, Json, |
193 | + body::BodyDataStream, |
194 | + extract::{Path, Query, Request, State}, |
195 | + http::StatusCode, |
196 | + response::Response, |
197 | }; |
198 | use bytes::Bytes; |
199 | use futures::{Stream, StreamExt, TryStreamExt}; |
200 | + use http::header::CONTENT_TYPE; |
201 | use oci_spec::{ |
202 | distribution::{Reference, TagList}, |
203 | - image::{Digest, ImageManifest}, |
204 | + image::{Digest, ImageManifest, MediaType}, |
205 | }; |
206 | use serde::Deserialize; |
207 | use serde_json::json; |
208 | @@ -52,11 +57,30 @@ pub async fn stat_blob( |
209 | } |
210 | } |
211 | |
212 | - pub async fn write_manifest() {} |
213 | + #[axum::debug_handler] |
214 | + pub async fn write_manifest( |
215 | + Extension(namespace): Extension<Namespace>, |
216 | + State(state): State<AppState>, |
217 | + Path(digest): Path<String>, |
218 | + headers: http::header::HeaderMap, |
219 | + axum::extract::Json(manifest): axum::extract::Json<ImageManifest>, |
220 | + ) -> Result<StatusCode, Error> { |
221 | + let digest = Digest::from_str(&digest)?; |
222 | + let Some(content_type) = headers.get(CONTENT_TYPE) else { |
223 | + todo!() |
224 | + }; |
225 | + let content_type_str = content_type.to_str().unwrap(); |
226 | + let media_type = MediaType::from(content_type_str); |
227 | + if !matches!(media_type, MediaType::ImageManifest) { |
228 | + todo!() |
229 | + } |
230 | + state.oci.write_manifest(&namespace, &manifest).await?; |
231 | + Ok(StatusCode::OK) |
232 | + } |
233 | |
234 | #[derive(Deserialize)] |
235 | pub struct UploadQuery { |
236 | - pub digest: String |
237 | + pub digest: String, |
238 | } |
239 | |
240 | #[axum::debug_handler] |
241 | @@ -71,7 +95,10 @@ pub async fn write_blob( |
242 | .map_err(|_| Error::Code(crate::error::Code::BlobUploadInvalid))?; |
243 | let stream = req.into_body().into_data_stream(); |
244 | let stream = stream.map_err(|e| Error::Stream(e.to_string())); |
245 | - state.oci.write_blob(Box::pin(stream), &uuid, &digest).await?; |
246 | + state |
247 | + .oci |
248 | + .write_blob(Box::pin(stream), &uuid, &digest) |
249 | + .await?; |
250 | Ok(StatusCode::OK) |
251 | } |
252 | |
253 | diff --git a/src/lib.rs b/src/lib.rs |
254 | index 2aac880..8566750 100644 |
255 | --- a/src/lib.rs |
256 | +++ b/src/lib.rs |
257 | @@ -3,6 +3,7 @@ use std::{fmt::Display, path::Path, str::FromStr}; |
258 | use error::Error; |
259 | use regex::Regex; |
260 | |
261 | + pub mod address; |
262 | pub mod error; |
263 | mod handlers; |
264 | pub mod oci_interface; |
265 | diff --git a/src/oci_interface.rs b/src/oci_interface.rs |
266 | index 9debc01..bf94810 100644 |
267 | --- a/src/oci_interface.rs |
268 | +++ b/src/oci_interface.rs |
269 | @@ -6,6 +6,7 @@ use oci_spec::{ |
270 | distribution::{Reference, TagList}, |
271 | image::{Digest, ImageManifest}, |
272 | }; |
273 | + use serde::Serialize; |
274 | use uuid::Uuid; |
275 | |
276 | use crate::{ |
277 | @@ -62,6 +63,11 @@ impl OciInterface { |
278 | Ok(()) |
279 | } |
280 | |
281 | + pub async fn write_manifest(&self, namespace: &Namespace, manifest: &ImageManifest) -> Result<(), Error> { |
282 | + let serialized = manifest.to_string().unwrap(); |
283 | + todo!() |
284 | + } |
285 | + |
286 | pub async fn read_manifest( |
287 | &self, |
288 | namespace: &Namespace, |
289 | diff --git a/src/routes.rs b/src/routes.rs |
290 | index 5b48a2a..d59ae6d 100644 |
291 | --- a/src/routes.rs |
292 | +++ b/src/routes.rs |
293 | @@ -60,6 +60,7 @@ pub fn router(storage: impl Storage + 'static) -> Router { |
294 | .route("/v2", get(crate::handlers::index)) |
295 | .route("/upload/{uuid}", put(crate::handlers::write_blob)) |
296 | .route("/blobs/uploads", post(crate::handlers::initiate_blob)) |
297 | + .route("/manifests/{digest}", put(crate::handlers::write_manifest)) |
298 | // // .route("/{name}/blobs/{digest}", head(crate::handlers::stat_blob)) |
299 | // // .route( |
300 | // // "/{name}/manifests/{reference}", |