Author:
Hash:
Timestamp:
+204 -6 +/-6 browse
Kevin Schoon [me@kevinschoon.com]
4f996b94f16d284a7cb2ab8f31482ab5ee82bbe4
Fri, 04 Apr 2025 14:44:44 +0000 (6 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}", |