Commit

Author:

Hash:

Timestamp:

+204 -6 +/-6 browse

Kevin Schoon [me@kevinschoon.com]

4f996b94f16d284a7cb2ab8f31482ab5ee82bbe4

Fri, 04 Apr 2025 14:44:44 +0000 (4 months ago)

implement new address module, misc routes
1diff --git a/Cargo.toml b/Cargo.toml
2index 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
15new file mode 100644
16index 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
184index 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
254index 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
266index 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
290index 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}",