Author:
Hash:
Timestamp:
+201 -82 +/-10 browse
Kevin Schoon [me@kevinschoon.com]
00247a62c414dfdbb041093d9acfc98460063433
Sat, 05 Apr 2025 15:38:46 +0000 (6 months ago)
| 1 | diff --git a/Cargo.lock b/Cargo.lock |
| 2 | index a1fce97..f20db56 100644 |
| 3 | --- a/Cargo.lock |
| 4 | +++ b/Cargo.lock |
| 5 | @@ -565,6 +565,7 @@ dependencies = [ |
| 6 | "http", |
| 7 | "oci-spec", |
| 8 | "regex", |
| 9 | + "relative-path", |
| 10 | "serde", |
| 11 | "serde_json", |
| 12 | "thiserror", |
| 13 | @@ -696,6 +697,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 14 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" |
| 15 | |
| 16 | [[package]] |
| 17 | + name = "relative-path" |
| 18 | + version = "1.9.3" |
| 19 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 20 | + checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" |
| 21 | + |
| 22 | + [[package]] |
| 23 | name = "rustc-demangle" |
| 24 | version = "0.1.24" |
| 25 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 26 | diff --git a/Cargo.toml b/Cargo.toml |
| 27 | index 2677521..55c172c 100644 |
| 28 | --- a/Cargo.toml |
| 29 | +++ b/Cargo.toml |
| 30 | @@ -18,6 +18,7 @@ uuid = { version = "1.16.0", features = ["v4"] } |
| 31 | http = "1.3.1" |
| 32 | tower = { version = "0.5.2", features = ["util"] } |
| 33 | bytes = "1.10.1" |
| 34 | + relative-path = "1.9.3" |
| 35 | |
| 36 | [dev-dependencies] |
| 37 | tokio = { version = "1.44.1", features = ["full"] } |
| 38 | diff --git a/src/address.rs b/src/address.rs |
| 39 | index ea16ed4..96edd5f 100644 |
| 40 | --- a/src/address.rs |
| 41 | +++ b/src/address.rs |
| 42 | @@ -4,12 +4,17 @@ use std::{ |
| 43 | }; |
| 44 | |
| 45 | use oci_spec::image::Digest; |
| 46 | + use relative_path::RelativePath; |
| 47 | use uuid::Uuid; |
| 48 | |
| 49 | use crate::Namespace; |
| 50 | |
| 51 | const SEPARATOR: &str = "/"; |
| 52 | |
| 53 | + pub trait Addressable { |
| 54 | + fn parts(&self) -> Vec<String>; |
| 55 | + } |
| 56 | + |
| 57 | /// Address is a path-like object for addressing OCI objects. The design of |
| 58 | /// this basically copies the file system layout used by the Docker |
| 59 | /// distribution registry. https://github.com/distribution/distribution |
| 60 | @@ -21,18 +26,25 @@ pub struct Address { |
| 61 | impl Address { |
| 62 | pub fn as_path(&self, base_dir: &Path) -> PathBuf { |
| 63 | let parts = self.parts.join(SEPARATOR); |
| 64 | - let path = Path::new(&parts); |
| 65 | - base_dir.join(path) |
| 66 | + RelativePath::new(&parts).to_path(base_dir) |
| 67 | } |
| 68 | |
| 69 | - pub fn link(&self) -> Self { |
| 70 | - let mut parts = self.parts.clone(); |
| 71 | + pub fn new(addr: &impl Addressable) -> Self { |
| 72 | + Address { |
| 73 | + parts: addr.parts(), |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + /// Create an addressable link |
| 78 | + pub fn link(addr: &impl Addressable) -> Self { |
| 79 | + let mut parts = addr.parts(); |
| 80 | parts.push(String::from("link")); |
| 81 | Address { parts } |
| 82 | } |
| 83 | |
| 84 | - pub fn data(&self) -> Self { |
| 85 | - let mut parts = self.parts.clone(); |
| 86 | + /// create an addressable data path |
| 87 | + pub fn data(addr: &impl Addressable) -> Self { |
| 88 | + let mut parts = addr.parts(); |
| 89 | parts.push(String::from("data")); |
| 90 | Address { parts } |
| 91 | } |
| 92 | @@ -49,17 +61,15 @@ pub struct TagDirectory<'a> { |
| 93 | pub namespace: &'a Namespace, |
| 94 | } |
| 95 | |
| 96 | - impl<'a> From<&'a Namespace> for TagDirectory<'a> { |
| 97 | - fn from(value: &'a Namespace) -> Self { |
| 98 | - TagDirectory { namespace: value } |
| 99 | + impl Addressable for TagDirectory<'_> { |
| 100 | + fn parts(&self) -> Vec<String> { |
| 101 | + vec![self.namespace.to_string(), String::from("tags")] |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | - impl From<TagDirectory<'_>> for Address { |
| 106 | - fn from(value: TagDirectory<'_>) -> Self { |
| 107 | - Address { |
| 108 | - parts: vec![value.namespace.to_string(), String::from("tags")], |
| 109 | - } |
| 110 | + impl<'a> From<&'a Namespace> for TagDirectory<'a> { |
| 111 | + fn from(value: &'a Namespace) -> Self { |
| 112 | + TagDirectory { namespace: value } |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | @@ -69,15 +79,13 @@ pub struct Tag<'a> { |
| 117 | pub name: &'a str, |
| 118 | } |
| 119 | |
| 120 | - impl From<Tag<'_>> for Address { |
| 121 | - fn from(value: Tag<'_>) -> Self { |
| 122 | - Address { |
| 123 | - parts: vec![ |
| 124 | - value.namespace.to_string(), |
| 125 | - String::from("tags"), |
| 126 | - value.name.to_string(), |
| 127 | - ], |
| 128 | - } |
| 129 | + impl Addressable for Tag<'_> { |
| 130 | + fn parts(&self) -> Vec<String> { |
| 131 | + vec![ |
| 132 | + self.namespace.to_string(), |
| 133 | + String::from("tags"), |
| 134 | + self.name.to_string(), |
| 135 | + ] |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | @@ -92,11 +100,9 @@ impl<'a> From<&'a Uuid> for TempBlob<'a> { |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | - impl From<TempBlob<'_>> for Address { |
| 144 | - fn from(value: TempBlob<'_>) -> Self { |
| 145 | - Address { |
| 146 | - parts: vec![String::from("tmp"), value.uuid.to_string()], |
| 147 | - } |
| 148 | + impl Addressable for TempBlob<'_> { |
| 149 | + fn parts(&self) -> Vec<String> { |
| 150 | + vec![String::from("tmp"), self.uuid.to_string()] |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | @@ -111,11 +117,11 @@ impl<'a> From<&'a Digest> for Blob<'a> { |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | - impl From<Blob<'_>> for Address { |
| 159 | - fn from(value: Blob<'_>) -> Self { |
| 160 | - let digest_str = value.digest.digest(); |
| 161 | + impl Addressable for Blob<'_> { |
| 162 | + fn parts(&self) -> Vec<String> { |
| 163 | + let digest_str = self.digest.digest(); |
| 164 | let first_two: String = digest_str.chars().take(2).collect(); |
| 165 | - let parts = match value.digest.algorithm() { |
| 166 | + match self.digest.algorithm() { |
| 167 | oci_spec::image::DigestAlgorithm::Sha256 => vec![ |
| 168 | String::from("blobs"), |
| 169 | String::from("sha256"), |
| 170 | @@ -126,8 +132,7 @@ impl From<Blob<'_>> for Address { |
| 171 | oci_spec::image::DigestAlgorithm::Sha512 => todo!(), |
| 172 | oci_spec::image::DigestAlgorithm::Other(_) => todo!(), |
| 173 | _ => todo!(), |
| 174 | - }; |
| 175 | - Address { parts } |
| 176 | + } |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | @@ -137,9 +142,14 @@ pub struct Manifest<'a> { |
| 181 | pub digest: &'a Digest, |
| 182 | } |
| 183 | |
| 184 | - impl From<Manifest<'_>> for Address { |
| 185 | - fn from(value: Manifest<'_>) -> Self { |
| 186 | - todo!() |
| 187 | + impl Addressable for Manifest<'_> { |
| 188 | + fn parts(&self) -> Vec<String> { |
| 189 | + vec![ |
| 190 | + self.namespace.to_string(), |
| 191 | + String::from("manifests"), |
| 192 | + self.digest.algorithm().to_string(), |
| 193 | + self.digest.digest().to_string(), |
| 194 | + ] |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | @@ -152,9 +162,9 @@ mod test { |
| 199 | #[test] |
| 200 | pub fn addresses() { |
| 201 | let namespace = Namespace::from_str("hello/world").unwrap(); |
| 202 | - assert!(Address::from(TagDirectory::from(&namespace)).to_string() == "hello/world/tags"); |
| 203 | + assert!(Address::new(&TagDirectory::from(&namespace)).to_string() == "hello/world/tags"); |
| 204 | assert!( |
| 205 | - Address::from(Tag { |
| 206 | + Address::new(&Tag { |
| 207 | namespace: &namespace, |
| 208 | name: "latest" |
| 209 | }) |
| 210 | @@ -162,14 +172,22 @@ mod test { |
| 211 | == "hello/world/tags/latest" |
| 212 | ); |
| 213 | let uuid = Uuid::new_v4(); |
| 214 | - assert!(Address::from(TempBlob::from(&uuid)).to_string() == format!("tmp/{}", uuid)); |
| 215 | + assert!(Address::new(&TempBlob::from(&uuid)).to_string() == format!("tmp/{}", uuid)); |
| 216 | let digest = Digest::from_str( |
| 217 | "sha256:57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f", |
| 218 | ) |
| 219 | .unwrap(); |
| 220 | assert!( |
| 221 | - Address::from(Blob { digest: &digest }).to_string() |
| 222 | + Address::new(&Blob { digest: &digest }).to_string() |
| 223 | == "blobs/sha256/57/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f" |
| 224 | + ); |
| 225 | + assert!( |
| 226 | + Address::new(&Manifest { |
| 227 | + namespace: &namespace, |
| 228 | + digest: &digest |
| 229 | + }) |
| 230 | + .to_string() |
| 231 | + == "hello/world/manifests/sha256/57f2ae062b76cff6f5a511fe6f907decfdefd6495e6afa31c44e0a6a1eca146f" |
| 232 | ) |
| 233 | } |
| 234 | } |
| 235 | diff --git a/src/error.rs b/src/error.rs |
| 236 | index 495f30b..ddc49ec 100644 |
| 237 | --- a/src/error.rs |
| 238 | +++ b/src/error.rs |
| 239 | @@ -1,4 +1,5 @@ |
| 240 | use axum::{Json, response::IntoResponse}; |
| 241 | + use http::StatusCode; |
| 242 | |
| 243 | use crate::storage::Error as StorageError; |
| 244 | |
| 245 | @@ -99,6 +100,17 @@ impl From<&Error> for Message { |
| 246 | impl IntoResponse for Error { |
| 247 | fn into_response(self) -> axum::response::Response { |
| 248 | let message = Message::from(&self); |
| 249 | - Json(message).into_response() |
| 250 | + let mut res = Json(message).into_response(); |
| 251 | + let status_code = res.status_mut(); |
| 252 | + *status_code = StatusCode::INTERNAL_SERVER_ERROR; |
| 253 | + tracing::error!("Server failure: {:?}", self); |
| 254 | + // match self { |
| 255 | + // Error::Code(code) => todo!(), |
| 256 | + // Error::Storage(error) => todo!(), |
| 257 | + // Error::OciInternal(oci_spec_error) => todo!(), |
| 258 | + // Error::OciParsing(parse_error) => todo!(), |
| 259 | + // Error::Stream(_) => todo!(), |
| 260 | + // } |
| 261 | + res |
| 262 | } |
| 263 | } |
| 264 | diff --git a/src/handlers.rs b/src/handlers.rs |
| 265 | index 1a1648e..fe33325 100644 |
| 266 | --- a/src/handlers.rs |
| 267 | +++ b/src/handlers.rs |
| 268 | @@ -9,7 +9,10 @@ use axum::{ |
| 269 | }; |
| 270 | use bytes::{Buf, Bytes}; |
| 271 | use futures::TryStreamExt; |
| 272 | - use http::header::CONTENT_TYPE; |
| 273 | + use http::{ |
| 274 | + HeaderValue, |
| 275 | + header::{CONTENT_LENGTH, CONTENT_TYPE}, |
| 276 | + }; |
| 277 | use oci_spec::{ |
| 278 | distribution::{Reference, TagList}, |
| 279 | image::{Digest, ImageManifest, MediaType}, |
| 280 | @@ -84,19 +87,16 @@ where |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | - /// NOTE: The registry MUST store the manifest in the exact byte representation |
| 285 | - /// provided by the client. |
| 286 | - #[axum::debug_handler] |
| 287 | pub async fn write_manifest( |
| 288 | Extension(namespace): Extension<Namespace>, |
| 289 | State(state): State<AppState>, |
| 290 | - Path(digest): Path<String>, |
| 291 | + Path(reference): Path<String>, |
| 292 | ManifestExtractor((manifest_bytes, manifest)): ManifestExtractor, |
| 293 | ) -> Result<StatusCode, Error> { |
| 294 | - let digest = Digest::from_str(&digest)?; |
| 295 | + let digest = manifest.config().digest(); |
| 296 | state |
| 297 | .oci |
| 298 | - .write_manifest(&namespace, &digest, &manifest, &manifest_bytes) |
| 299 | + .write_manifest(&namespace, digest, &reference, &manifest_bytes) |
| 300 | .await?; |
| 301 | Ok(StatusCode::OK) |
| 302 | } |
| 303 | @@ -106,6 +106,7 @@ pub struct UploadQuery { |
| 304 | pub digest: String, |
| 305 | } |
| 306 | |
| 307 | + /// Write a complete blob in one request |
| 308 | pub async fn write_blob( |
| 309 | State(state): State<AppState>, |
| 310 | Path(upload_uuid): Path<String>, |
| 311 | @@ -117,23 +118,72 @@ pub async fn write_blob( |
| 312 | .map_err(|_| Error::Code(crate::error::Code::BlobUploadInvalid))?; |
| 313 | let stream = req.into_body().into_data_stream(); |
| 314 | let stream = stream.map_err(|e| Error::Stream(e.to_string())); |
| 315 | - state |
| 316 | - .oci |
| 317 | - .write_blob(Box::pin(stream), &uuid, &digest) |
| 318 | - .await?; |
| 319 | + state.oci.write_chunk(Box::pin(stream), &uuid).await?; |
| 320 | + state.oci.commit_blob(&uuid, &digest).await?; |
| 321 | Ok(StatusCode::OK) |
| 322 | } |
| 323 | |
| 324 | + pub async fn write_blob_chunk( |
| 325 | + State(state): State<AppState>, |
| 326 | + Path(upload_uuid): Path<String>, |
| 327 | + req: Request, |
| 328 | + ) -> Result<Response, Error> { |
| 329 | + let uuid = Uuid::from_str(&upload_uuid) |
| 330 | + .map_err(|_| Error::Code(crate::error::Code::BlobUploadInvalid))?; |
| 331 | + let stream = req.into_body().into_data_stream(); |
| 332 | + let stream = stream.map_err(|e| Error::Stream(e.to_string())); |
| 333 | + state.oci.write_chunk(Box::pin(stream), &uuid).await?; |
| 334 | + let mut res = Response::new(axum::body::Body::empty()); |
| 335 | + let status = res.status_mut(); |
| 336 | + *status = StatusCode::ACCEPTED; |
| 337 | + let uuid_str = uuid.to_string(); |
| 338 | + let uri = format!("/v2/upload/{}", uuid_str); |
| 339 | + let headers = res.headers_mut(); |
| 340 | + headers.insert("Location", uri.parse().unwrap()); |
| 341 | + Ok(res) |
| 342 | + } |
| 343 | + |
| 344 | + pub async fn close_blob( |
| 345 | + State(state): State<AppState>, |
| 346 | + Path(upload_uuid): Path<String>, |
| 347 | + Query(query): Query<UploadQuery>, |
| 348 | + req: Request, |
| 349 | + ) -> Result<StatusCode, Error> { |
| 350 | + let digest = Digest::from_str(&query.digest)?; |
| 351 | + let uuid = Uuid::from_str(&upload_uuid) |
| 352 | + .map_err(|_| Error::Code(crate::error::Code::BlobUploadInvalid))?; |
| 353 | + let headers = req.headers(); |
| 354 | + if headers.get(CONTENT_LENGTH).is_some() { |
| 355 | + let stream = req.into_body().into_data_stream(); |
| 356 | + let stream = stream.map_err(|e| Error::Stream(e.to_string())); |
| 357 | + state.oci.write_chunk(Box::pin(stream), &uuid).await?; |
| 358 | + } |
| 359 | + state.oci.commit_blob(&uuid, &digest).await?; |
| 360 | + Ok(StatusCode::CREATED) |
| 361 | + } |
| 362 | + |
| 363 | + const OCI_CHUNK_MIN_LENGTH: usize = 10_000_000; |
| 364 | + |
| 365 | + /// Initiate a blob upload |
| 366 | + /// FIXME: Per the spec for chunked uploads a header of Content-Length: 0 |
| 367 | + /// MUST be present (for some reason). |
| 368 | pub async fn initiate_blob( |
| 369 | State(state): State<AppState>, |
| 370 | Extension(namespace): Extension<Namespace>, |
| 371 | ) -> Result<Response, Error> { |
| 372 | let uuid = state.oci.new_blob(&namespace).await?; |
| 373 | let mut res = Response::new(axum::body::Body::empty()); |
| 374 | + let status = res.status_mut(); |
| 375 | + *status = StatusCode::ACCEPTED; |
| 376 | let headers = res.headers_mut(); |
| 377 | let uuid_str = uuid.to_string(); |
| 378 | let uri = format!("/v2/upload/{}", uuid_str); |
| 379 | headers.insert("Location", uri.parse().unwrap()); |
| 380 | + // FIXME: make configurable |
| 381 | + headers.insert( |
| 382 | + "OCI-Chunk-Min-Length", |
| 383 | + HeaderValue::from_str(&OCI_CHUNK_MIN_LENGTH.to_string()).unwrap(), |
| 384 | + ); |
| 385 | Ok(res) |
| 386 | } |
| 387 | |
| 388 | diff --git a/src/lib.rs b/src/lib.rs |
| 389 | index 8566750..f41d040 100644 |
| 390 | --- a/src/lib.rs |
| 391 | +++ b/src/lib.rs |
| 392 | @@ -2,6 +2,7 @@ use std::{fmt::Display, path::Path, str::FromStr}; |
| 393 | |
| 394 | use error::Error; |
| 395 | use regex::Regex; |
| 396 | + use relative_path::RelativePath; |
| 397 | |
| 398 | pub mod address; |
| 399 | pub mod error; |
| 400 | @@ -21,8 +22,8 @@ const NAME_REGEXP_MATCH: &str = |
| 401 | pub struct Namespace(String); |
| 402 | |
| 403 | impl Namespace { |
| 404 | - pub fn path(&self) -> &Path { |
| 405 | - Path::new(&self.0) |
| 406 | + pub fn path(&self) -> &RelativePath { |
| 407 | + RelativePath::new(&self.0) |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | @@ -45,6 +46,12 @@ impl Display for Namespace { |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | + impl AsRef<str> for Namespace { |
| 416 | + fn as_ref(&self) -> &str { |
| 417 | + &self.0 |
| 418 | + } |
| 419 | + } |
| 420 | + |
| 421 | #[cfg(test)] |
| 422 | mod test { |
| 423 | use super::*; |
| 424 | diff --git a/src/oci_interface.rs b/src/oci_interface.rs |
| 425 | index 769095a..7e7ae67 100644 |
| 426 | --- a/src/oci_interface.rs |
| 427 | +++ b/src/oci_interface.rs |
| 428 | @@ -6,7 +6,6 @@ use oci_spec::{ |
| 429 | distribution::{Reference, TagList}, |
| 430 | image::{Digest, ImageManifest}, |
| 431 | }; |
| 432 | - use serde::Serialize; |
| 433 | use uuid::Uuid; |
| 434 | |
| 435 | use crate::{ |
| 436 | @@ -33,34 +32,34 @@ impl OciInterface { |
| 437 | pub async fn new_blob(&self, namespace: &Namespace) -> Result<Uuid, Error> { |
| 438 | let uuid = Uuid::new_v4(); |
| 439 | self.storage |
| 440 | - .write_all(&TempBlob::from(&uuid).into(), &[]) |
| 441 | + .write_all(&Address::new(&TempBlob::from(&uuid)), &[]) |
| 442 | .await |
| 443 | .map_err(Error::Storage)?; |
| 444 | Ok(uuid) |
| 445 | } |
| 446 | |
| 447 | - pub async fn write_blob<S>( |
| 448 | - &self, |
| 449 | - mut stream: Pin<Box<S>>, |
| 450 | - uuid: &Uuid, |
| 451 | - digest: &Digest, |
| 452 | - ) -> Result<(), Error> |
| 453 | + pub async fn commit_blob(&self, uuid: &Uuid, digest: &Digest) -> Result<(), Error> { |
| 454 | + let tmp_blob_addr = Address::new(&TempBlob::from(uuid)); |
| 455 | + let blob_addr = Address::data(&Blob::from(digest)); |
| 456 | + self.storage |
| 457 | + .mv(&tmp_blob_addr, &blob_addr) |
| 458 | + .await |
| 459 | + .map_err(Error::Storage)?; |
| 460 | + Ok(()) |
| 461 | + } |
| 462 | + |
| 463 | + pub async fn write_chunk<S>(&self, mut stream: Pin<Box<S>>, uuid: &Uuid) -> Result<(), Error> |
| 464 | where |
| 465 | S: Stream<Item = Result<Bytes, Error>>, |
| 466 | { |
| 467 | - let tmp_blob: Address = TempBlob::from(uuid).into(); |
| 468 | + let tmp_blob_addr = Address::new(&TempBlob::from(uuid)); |
| 469 | while let Some(item) = stream.next().await { |
| 470 | let chunk = item?.to_vec(); |
| 471 | self.storage |
| 472 | - .write(&tmp_blob, chunk.as_slice()) |
| 473 | + .write(&tmp_blob_addr, chunk.as_slice()) |
| 474 | .await |
| 475 | .map_err(Error::Storage)?; |
| 476 | } |
| 477 | - let blob: Address = Blob::from(digest).into(); |
| 478 | - self.storage |
| 479 | - .mv(&tmp_blob, &blob.data()) |
| 480 | - .await |
| 481 | - .map_err(Error::Storage)?; |
| 482 | Ok(()) |
| 483 | } |
| 484 | |
| 485 | @@ -68,22 +67,41 @@ impl OciInterface { |
| 486 | &self, |
| 487 | namespace: &Namespace, |
| 488 | digest: &Digest, |
| 489 | - manifest: &ImageManifest, |
| 490 | + tag: &str, |
| 491 | manifest_bytes: &Bytes, |
| 492 | ) -> Result<(), Error> { |
| 493 | - let blob: Address = Blob::from(digest).into(); |
| 494 | + let tmp_blob_addr = Address::new(&TempBlob::from(&Uuid::new_v4())); |
| 495 | + self.storage |
| 496 | + .write_all(&tmp_blob_addr, manifest_bytes.to_vec().as_slice()) |
| 497 | + .await |
| 498 | + .map_err(Error::Storage)?; |
| 499 | + let blob_address = &Address::data(&Blob::from(digest)); |
| 500 | self.storage |
| 501 | - .write_all(&blob.data(), manifest_bytes.to_vec().as_slice()) |
| 502 | + .mv(&tmp_blob_addr, blob_address) |
| 503 | .await |
| 504 | .map_err(Error::Storage)?; |
| 505 | + let tag_addr = &Address::link(&crate::address::Tag { |
| 506 | + namespace, |
| 507 | + name: tag, |
| 508 | + }); |
| 509 | self.storage |
| 510 | - .write_all( |
| 511 | - &crate::address::Manifest { namespace, digest }.into(), |
| 512 | - digest.to_string().as_bytes(), |
| 513 | - ) |
| 514 | + .write_all(tag_addr, digest.to_string().as_bytes()) |
| 515 | .await |
| 516 | .map_err(Error::Storage)?; |
| 517 | Ok(()) |
| 518 | + // let blob_addr = Address::data(&Blob::from(digest)); |
| 519 | + // self.storage |
| 520 | + // .write_all(&blob_addr, manifest_bytes.to_vec().as_slice()) |
| 521 | + // .await |
| 522 | + // .map_err(Error::Storage)?; |
| 523 | + // self.storage |
| 524 | + // .write_all( |
| 525 | + // &Address::link(&crate::address::Manifest { namespace, digest }), |
| 526 | + // digest.to_string().as_bytes(), |
| 527 | + // ) |
| 528 | + // .await |
| 529 | + // .map_err(Error::Storage)?; |
| 530 | + // Ok(()) |
| 531 | } |
| 532 | |
| 533 | pub async fn read_manifest( |
| 534 | @@ -106,9 +124,9 @@ impl OciInterface { |
| 535 | } |
| 536 | |
| 537 | pub async fn has_blob(&self, digest: &Digest) -> Result<bool, Error> { |
| 538 | - let address: Address = Blob::from(digest).into(); |
| 539 | + let blob_addr = Address::data(&Blob::from(digest)); |
| 540 | self.storage |
| 541 | - .exists(&address.data()) |
| 542 | + .exists(&blob_addr) |
| 543 | .await |
| 544 | .map_err(Error::Storage) |
| 545 | } |
| 546 | diff --git a/src/routes.rs b/src/routes.rs |
| 547 | index fd1e36c..ffece69 100644 |
| 548 | --- a/src/routes.rs |
| 549 | +++ b/src/routes.rs |
| 550 | @@ -3,7 +3,7 @@ use std::str::FromStr; |
| 551 | use std::sync::Arc; |
| 552 | |
| 553 | use axum::extract::{DefaultBodyLimit, Request}; |
| 554 | - use axum::routing::{head, post, put}; |
| 555 | + use axum::routing::{head, patch, post, put}; |
| 556 | use axum::{Router, routing::get}; |
| 557 | use http::Uri; |
| 558 | |
| 559 | @@ -60,7 +60,12 @@ const MAXIMUM_MANIFEST_SIZE: usize = 5_000_000; |
| 560 | pub fn router(storage: impl Storage + 'static) -> Router { |
| 561 | Router::new() |
| 562 | .route("/v2", get(crate::handlers::index)) |
| 563 | - .route("/upload/{uuid}", put(crate::handlers::write_blob)) |
| 564 | + .route( |
| 565 | + "/upload/{reference}", |
| 566 | + patch(crate::handlers::write_blob_chunk), |
| 567 | + ) |
| 568 | + .route("/upload/{reference}", post(crate::handlers::write_blob)) |
| 569 | + .route("/upload/{reference}", put(crate::handlers::close_blob)) |
| 570 | .route("/blobs/uploads", post(crate::handlers::initiate_blob)) |
| 571 | .route("/blobs/{digest}", head(crate::handlers::stat_blob)) |
| 572 | .route( |
| 573 | diff --git a/src/storage.rs b/src/storage.rs |
| 574 | index 88339a8..0391647 100644 |
| 575 | --- a/src/storage.rs |
| 576 | +++ b/src/storage.rs |
| 577 | @@ -1,4 +1,4 @@ |
| 578 | - use std::{io::Error as IoError, path::Path}; |
| 579 | + use std::{io::Error as IoError}; |
| 580 | |
| 581 | use crate::address::Address; |
| 582 | |
| 583 | diff --git a/src/storage_fs.rs b/src/storage_fs.rs |
| 584 | index 16a9894..faeb078 100644 |
| 585 | --- a/src/storage_fs.rs |
| 586 | +++ b/src/storage_fs.rs |
| 587 | @@ -59,6 +59,7 @@ impl Storage for FileSystem { |
| 588 | /// } |
| 589 | |
| 590 | async fn write_all(&self, addr: &Address, bytes: &[u8]) -> Result<(), Error> { |
| 591 | + tracing::info!("Writing blob to {}", addr); |
| 592 | let path = addr.as_path(&self.base); |
| 593 | self.ensure_dir(&path).await?; |
| 594 | let mut fp = tokio::fs::OpenOptions::new() |