Rust implementation of OCI Distribution Spec with granular access control

add blob and manifest delete

+172 -22
+66 -13
src/blobs.rs
··· 10 10 // | end-11 | `POST` | `/v2/<name>/blobs/uploads/?mount=<digest>&from=<other_name>` | `201` | `404` | 11 11 12 12 use serde::Deserialize; 13 - use serde_json::{json, Value}; 14 13 use std::sync::Arc; 15 14 16 15 use crate::{ ··· 21 20 body::Body, 22 21 extract::{Path, Query, State}, 23 22 http::{HeaderMap, StatusCode}, 24 - response::{Json, Response}, 23 + response::Response, 25 24 }; 26 25 use bytes::Bytes; 27 26 ··· 348 347 349 348 // end-10 DELETE /v2/:name/blobs/:digest 350 349 pub(crate) async fn delete_blob_by_digest( 351 - State(data): State<Arc<state::App>>, 352 - Path(name): Path<String>, 353 - Path(digest): Path<String>, 354 - ) -> Json<Value> { 355 - let status = data.server_status.lock().await; 350 + State(state): State<Arc<state::App>>, 351 + Path((org, repo, digest_string)): Path<(String, String, String)>, 352 + headers: HeaderMap, 353 + ) -> Response<Body> { 354 + let host = &state.args.host; 355 + 356 + // Authenticate 357 + if auth::get(State(state.clone()), headers).await.status() != StatusCode::OK { 358 + return Response::builder() 359 + .status(StatusCode::UNAUTHORIZED) 360 + .header( 361 + "WWW-Authenticate", 362 + format!("Basic realm=\"{}\", charset=\"UTF-8\"", host), 363 + ) 364 + .body(Body::from("401 Unauthorized")) 365 + .unwrap(); 366 + } 367 + 368 + // Clean digest (strip sha256: prefix if present) 369 + let clean_digest = digest_string 370 + .strip_prefix("sha256:") 371 + .unwrap_or(&digest_string); 372 + 356 373 log::info!( 357 - "blobs/delete_blob_by_digest: name: {}, digest: {}", 358 - name, 359 - digest 374 + "blobs/delete_blob_by_digest: org: {}, repo: {}, digest: {}", 375 + org, 376 + repo, 377 + clean_digest 360 378 ); 361 - Json(json!({ 362 - "not_implemented": format!("name {} digest {} server_status {}", name, digest, status) 363 - })) 379 + 380 + // Delete blob 381 + match storage::delete_blob(&org, &repo, clean_digest) { 382 + Ok(()) => { 383 + log::info!("Deleted blob {}/{}/{}", org, repo, clean_digest); 384 + 385 + Response::builder() 386 + .status(StatusCode::ACCEPTED) 387 + .body(Body::empty()) 388 + .unwrap() 389 + } 390 + Err(e) => { 391 + if e.kind() == std::io::ErrorKind::NotFound { 392 + log::warn!( 393 + "Attempted to delete non-existent blob {}/{}/{}", 394 + org, 395 + repo, 396 + clean_digest 397 + ); 398 + Response::builder() 399 + .status(StatusCode::NOT_FOUND) 400 + .body(Body::from("404 Not Found")) 401 + .unwrap() 402 + } else { 403 + log::error!( 404 + "Failed to delete blob {}/{}/{}: {}", 405 + org, 406 + repo, 407 + clean_digest, 408 + e 409 + ); 410 + Response::builder() 411 + .status(StatusCode::INTERNAL_SERVER_ERROR) 412 + .body(Body::from("Internal server error")) 413 + .unwrap() 414 + } 415 + } 416 + } 364 417 }
+62 -9
src/manifests.rs
··· 188 188 189 189 // end-9 DELETE /v2/:name/manifests/:reference 190 190 pub(crate) async fn delete_manifest_by_reference( 191 - Path(name): Path<String>, 192 - Path(reference): Path<String>, 191 + State(state): State<Arc<state::App>>, 192 + Path((org, repo, reference)): Path<(String, String, String)>, 193 + headers: HeaderMap, 193 194 ) -> Response<Body> { 195 + let host = &state.args.host; 196 + 197 + // Authenticate 198 + if auth::get(State(state.clone()), headers).await.status() != StatusCode::OK { 199 + return Response::builder() 200 + .status(StatusCode::UNAUTHORIZED) 201 + .header( 202 + "WWW-Authenticate", 203 + format!("Basic realm=\"{}\", charset=\"UTF-8\"", host), 204 + ) 205 + .body(Body::from("401 Unauthorized")) 206 + .unwrap(); 207 + } 208 + 209 + // Clean reference (strip sha256: prefix if present) 210 + let clean_reference = reference.strip_prefix("sha256:").unwrap_or(&reference); 211 + 194 212 log::info!( 195 - "manifests/delete_manifest_by_reference: name: {}, reference: {}", 196 - name, 197 - reference 213 + "manifests/delete_manifest_by_reference: org: {}, repo: {}, reference: {}", 214 + org, 215 + repo, 216 + clean_reference 198 217 ); 199 - Response::builder() 200 - .status(StatusCode::NOT_IMPLEMENTED) 201 - .body(Body::from("501 Not Implemented")) 202 - .unwrap() 218 + 219 + // Delete manifest 220 + match storage::delete_manifest(&org, &repo, clean_reference) { 221 + Ok(()) => { 222 + log::info!("Deleted manifest {}/{}/{}", org, repo, clean_reference); 223 + 224 + Response::builder() 225 + .status(StatusCode::ACCEPTED) 226 + .body(Body::empty()) 227 + .unwrap() 228 + } 229 + Err(e) => { 230 + if e.kind() == std::io::ErrorKind::NotFound { 231 + log::warn!( 232 + "Attempted to delete non-existent manifest {}/{}/{}", 233 + org, 234 + repo, 235 + clean_reference 236 + ); 237 + Response::builder() 238 + .status(StatusCode::NOT_FOUND) 239 + .body(Body::from("404 Not Found")) 240 + .unwrap() 241 + } else { 242 + log::error!( 243 + "Failed to delete manifest {}/{}/{}: {}", 244 + org, 245 + repo, 246 + clean_reference, 247 + e 248 + ); 249 + Response::builder() 250 + .status(StatusCode::INTERNAL_SERVER_ERROR) 251 + .body(Body::from("Internal server error")) 252 + .unwrap() 253 + } 254 + } 255 + } 203 256 }
+44
src/storage.rs
··· 275 275 ); 276 276 std::fs::remove_file(upload_path) 277 277 } 278 + 279 + pub(crate) fn delete_manifest( 280 + org: &str, 281 + repo: &str, 282 + reference: &str, 283 + ) -> Result<(), std::io::Error> { 284 + let sanitized_org = sanitize_string(org); 285 + let sanitized_repo = sanitize_string(repo); 286 + let sanitized_reference = sanitize_string(reference); 287 + 288 + let manifest_path = format!( 289 + "./tmp/manifests/{}/{}/{}", 290 + sanitized_org, sanitized_repo, sanitized_reference 291 + ); 292 + 293 + if !std::path::Path::new(&manifest_path).exists() { 294 + return Err(std::io::Error::new( 295 + std::io::ErrorKind::NotFound, 296 + "Manifest not found", 297 + )); 298 + } 299 + 300 + std::fs::remove_file(manifest_path) 301 + } 302 + 303 + pub(crate) fn delete_blob(org: &str, repo: &str, digest: &str) -> Result<(), std::io::Error> { 304 + let sanitized_org = sanitize_string(org); 305 + let sanitized_repo = sanitize_string(repo); 306 + let sanitized_digest = sanitize_string(digest); 307 + 308 + let blob_path = format!( 309 + "./tmp/blobs/{}/{}/{}", 310 + sanitized_org, sanitized_repo, sanitized_digest 311 + ); 312 + 313 + if !std::path::Path::new(&blob_path).exists() { 314 + return Err(std::io::Error::new( 315 + std::io::ErrorKind::NotFound, 316 + "Blob not found", 317 + )); 318 + } 319 + 320 + std::fs::remove_file(blob_path) 321 + }