Rust implementation of OCI Distribution Spec with granular access control
at main 373 lines 11 kB view raw
1use axum::body::Body; 2use std::{ 3 fs::{create_dir_all, File}, 4 io::Write, 5}; 6 7pub(crate) fn sanitize_string(input: &str) -> String { 8 input 9 .chars() 10 .map(|c| { 11 if c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' || c == '/' { 12 c 13 } else { 14 '_' 15 } 16 }) 17 .collect() 18} 19 20pub(crate) async fn write_blob(org: &str, repo: &str, req_digest_string: &str, body: Body) -> bool { 21 let bytes_res = axum::body::to_bytes(body, usize::MAX).await; 22 if bytes_res.is_err() { 23 return false; 24 } 25 let bytes = bytes_res.unwrap(); 26 27 let req_digest = req_digest_string 28 .strip_prefix("sha256:") 29 .unwrap_or(req_digest_string); 30 let body_digest = sha256::digest(bytes.as_ref()); 31 let matches = req_digest == body_digest; 32 33 log::info!( 34 "storage/write_file: digest: {}, body_digest: {}, matches: {}", 35 req_digest, 36 body_digest, 37 matches 38 ); 39 40 if !matches { 41 return false; 42 } 43 44 let base_path = format!( 45 "./tmp/blobs/{}/{}", 46 sanitize_string(org), 47 sanitize_string(repo), 48 ); 49 50 write_bytes_to_file(&base_path, req_digest, &bytes).await 51} 52 53pub(crate) async fn write_manifest_bytes( 54 org: &str, 55 repo: &str, 56 reference: &str, 57 bytes: &[u8], 58) -> bool { 59 let base_path = format!( 60 "./tmp/manifests/{}/{}", 61 sanitize_string(org), 62 sanitize_string(repo), 63 ); 64 65 write_bytes_to_file(&base_path, reference, bytes).await 66} 67 68pub(crate) async fn write_bytes_to_file(base_path: &str, file_name: &str, bytes: &[u8]) -> bool { 69 if let Err(e) = create_dir_all(base_path) { 70 log::error!("storage/write_file: error creating directory: {}", e); 71 return false; 72 } 73 74 let mut file = match File::create(format!("{}/{}", base_path, file_name)) { 75 Ok(file) => file, 76 Err(e) => { 77 log::error!("storage/write_file: error creating file: {}", e); 78 return false; 79 } 80 }; 81 82 if let Err(e) = file.write_all(bytes) { 83 log::error!("storage/write_file: error writing to file: {}", e); 84 return false; 85 } 86 87 if let Err(e) = file.flush() { 88 log::error!("storage/write_file: error flushing file: {}", e); 89 return false; 90 } 91 92 log::info!("storage/write_file: wrote to {}", base_path); 93 94 true 95} 96 97pub(crate) fn read_blob(org: &str, repo: &str, digest: &str) -> Result<Vec<u8>, std::io::Error> { 98 let sanitized_org = sanitize_string(org); 99 let sanitized_repo = sanitize_string(repo); 100 let sanitized_digest = sanitize_string(digest); 101 102 let blob_path = format!( 103 "./tmp/blobs/{}/{}/{}", 104 sanitized_org, sanitized_repo, sanitized_digest 105 ); 106 std::fs::read(blob_path) 107} 108 109pub(crate) fn blob_metadata( 110 org: &str, 111 repo: &str, 112 digest: &str, 113) -> Result<std::fs::Metadata, std::io::Error> { 114 let sanitized_org = sanitize_string(org); 115 let sanitized_repo = sanitize_string(repo); 116 let sanitized_digest = sanitize_string(digest); 117 118 let blob_path = format!( 119 "./tmp/blobs/{}/{}/{}", 120 sanitized_org, sanitized_repo, sanitized_digest 121 ); 122 std::fs::metadata(blob_path) 123} 124 125pub(crate) fn read_manifest( 126 org: &str, 127 repo: &str, 128 reference: &str, 129) -> Result<Vec<u8>, std::io::Error> { 130 let sanitized_org = sanitize_string(org); 131 let sanitized_repo = sanitize_string(repo); 132 let sanitized_reference = sanitize_string(reference); 133 134 let manifest_path = format!( 135 "./tmp/manifests/{}/{}/{}", 136 sanitized_org, sanitized_repo, sanitized_reference 137 ); 138 std::fs::read(manifest_path) 139} 140 141pub(crate) fn manifest_exists(org: &str, repo: &str, reference: &str) -> bool { 142 let sanitized_org = sanitize_string(org); 143 let sanitized_repo = sanitize_string(repo); 144 let sanitized_reference = sanitize_string(reference); 145 146 let manifest_path = format!( 147 "./tmp/manifests/{}/{}/{}", 148 sanitized_org, sanitized_repo, sanitized_reference 149 ); 150 std::path::Path::new(&manifest_path).exists() 151} 152 153pub(crate) fn list_tags(org: &str, repo: &str) -> Result<Vec<String>, std::io::Error> { 154 let sanitized_org = sanitize_string(org); 155 let sanitized_repo = sanitize_string(repo); 156 157 let manifests_dir = format!("./tmp/manifests/{}/{}", sanitized_org, sanitized_repo); 158 let path = std::path::Path::new(&manifests_dir); 159 160 if !path.exists() { 161 return Ok(Vec::new()); 162 } 163 164 let mut tags = Vec::new(); 165 166 for entry in std::fs::read_dir(path)? { 167 let entry = entry?; 168 if entry.path().is_file() { 169 if let Some(filename) = entry.file_name().to_str() { 170 // Filter out digest references (64-char hex strings or sha256: prefixed) 171 // Only include tag names 172 let is_digest = filename.starts_with("sha256:") 173 || (filename.len() == 64 && filename.chars().all(|c| c.is_ascii_hexdigit())); 174 175 if !is_digest { 176 tags.push(filename.to_string()); 177 } 178 } 179 } 180 } 181 182 // Sort tags alphabetically for consistent ordering 183 tags.sort(); 184 Ok(tags) 185} 186 187pub(crate) fn init_upload_session(org: &str, repo: &str, uuid: &str) -> Result<(), std::io::Error> { 188 let sanitized_org = sanitize_string(org); 189 let sanitized_repo = sanitize_string(repo); 190 let sanitized_uuid = sanitize_string(uuid); 191 192 let upload_dir = format!("./tmp/uploads/{}/{}", sanitized_org, sanitized_repo); 193 std::fs::create_dir_all(&upload_dir)?; 194 195 let upload_path = format!("{}/{}", upload_dir, sanitized_uuid); 196 std::fs::File::create(upload_path)?; 197 Ok(()) 198} 199 200pub(crate) fn append_upload_chunk( 201 org: &str, 202 repo: &str, 203 uuid: &str, 204 chunk_data: &[u8], 205) -> Result<u64, std::io::Error> { 206 use std::fs::OpenOptions; 207 208 let sanitized_org = sanitize_string(org); 209 let sanitized_repo = sanitize_string(repo); 210 let sanitized_uuid = sanitize_string(uuid); 211 212 let upload_path = format!( 213 "./tmp/uploads/{}/{}/{}", 214 sanitized_org, sanitized_repo, sanitized_uuid 215 ); 216 217 let mut file = OpenOptions::new().append(true).open(&upload_path)?; 218 219 file.write_all(chunk_data)?; 220 221 let metadata = std::fs::metadata(&upload_path)?; 222 Ok(metadata.len()) 223} 224 225pub(crate) fn finalize_upload( 226 org: &str, 227 repo: &str, 228 uuid: &str, 229 expected_digest: &str, 230) -> Result<String, String> { 231 let sanitized_org = sanitize_string(org); 232 let sanitized_repo = sanitize_string(repo); 233 let sanitized_uuid = sanitize_string(uuid); 234 235 let upload_path = format!( 236 "./tmp/uploads/{}/{}/{}", 237 sanitized_org, sanitized_repo, sanitized_uuid 238 ); 239 240 let upload_data = 241 std::fs::read(&upload_path).map_err(|e| format!("Failed to read upload: {}", e))?; 242 243 let actual_digest = sha256::digest(&upload_data); 244 let clean_expected = expected_digest 245 .strip_prefix("sha256:") 246 .unwrap_or(expected_digest); 247 248 if actual_digest != clean_expected { 249 return Err(format!( 250 "Digest mismatch: expected {}, got {}", 251 clean_expected, actual_digest 252 )); 253 } 254 255 let blob_dir = format!("./tmp/blobs/{}/{}", sanitized_org, sanitized_repo); 256 std::fs::create_dir_all(&blob_dir).map_err(|e| format!("Failed to create blob dir: {}", e))?; 257 258 let blob_path = format!("{}/{}", blob_dir, actual_digest); 259 std::fs::rename(&upload_path, &blob_path) 260 .map_err(|e| format!("Failed to move upload to blob: {}", e))?; 261 262 Ok(actual_digest) 263} 264 265pub(crate) fn delete_upload_session( 266 org: &str, 267 repo: &str, 268 uuid: &str, 269) -> Result<(), std::io::Error> { 270 let sanitized_org = sanitize_string(org); 271 let sanitized_repo = sanitize_string(repo); 272 let sanitized_uuid = sanitize_string(uuid); 273 274 let upload_path = format!( 275 "./tmp/uploads/{}/{}/{}", 276 sanitized_org, sanitized_repo, sanitized_uuid 277 ); 278 std::fs::remove_file(upload_path) 279} 280 281pub(crate) fn delete_manifest( 282 org: &str, 283 repo: &str, 284 reference: &str, 285) -> Result<(), std::io::Error> { 286 let sanitized_org = sanitize_string(org); 287 let sanitized_repo = sanitize_string(repo); 288 let sanitized_reference = sanitize_string(reference); 289 290 let manifest_path = format!( 291 "./tmp/manifests/{}/{}/{}", 292 sanitized_org, sanitized_repo, sanitized_reference 293 ); 294 295 if !std::path::Path::new(&manifest_path).exists() { 296 return Err(std::io::Error::new( 297 std::io::ErrorKind::NotFound, 298 "Manifest not found", 299 )); 300 } 301 302 std::fs::remove_file(manifest_path) 303} 304 305pub(crate) fn delete_blob(org: &str, repo: &str, digest: &str) -> Result<(), std::io::Error> { 306 let sanitized_org = sanitize_string(org); 307 let sanitized_repo = sanitize_string(repo); 308 let sanitized_digest = sanitize_string(digest); 309 310 let blob_path = format!( 311 "./tmp/blobs/{}/{}/{}", 312 sanitized_org, sanitized_repo, sanitized_digest 313 ); 314 315 if !std::path::Path::new(&blob_path).exists() { 316 return Err(std::io::Error::new( 317 std::io::ErrorKind::NotFound, 318 "Blob not found", 319 )); 320 } 321 322 std::fs::remove_file(blob_path) 323} 324 325pub(crate) fn mount_blob( 326 source_org: &str, 327 source_repo: &str, 328 target_org: &str, 329 target_repo: &str, 330 digest: &str, 331) -> Result<(), std::io::Error> { 332 let sanitized_source_org = sanitize_string(source_org); 333 let sanitized_source_repo = sanitize_string(source_repo); 334 let sanitized_target_org = sanitize_string(target_org); 335 let sanitized_target_repo = sanitize_string(target_repo); 336 let sanitized_digest = sanitize_string(digest); 337 338 // Check if blob exists in source repository 339 let source_path = format!( 340 "./tmp/blobs/{}/{}/{}", 341 sanitized_source_org, sanitized_source_repo, sanitized_digest 342 ); 343 344 if !std::path::Path::new(&source_path).exists() { 345 return Err(std::io::Error::new( 346 std::io::ErrorKind::NotFound, 347 "Source blob not found", 348 )); 349 } 350 351 // Create target directory 352 let target_dir = format!( 353 "./tmp/blobs/{}/{}", 354 sanitized_target_org, sanitized_target_repo 355 ); 356 std::fs::create_dir_all(&target_dir)?; 357 358 // Create target path 359 let target_path = format!("{}/{}", target_dir, sanitized_digest); 360 361 // If target already exists, that's fine (already mounted) 362 if std::path::Path::new(&target_path).exists() { 363 return Ok(()); 364 } 365 366 // Try hard link first (most efficient - no data duplication) 367 if std::fs::hard_link(&source_path, &target_path).is_err() { 368 // If hard link fails (cross-device), copy the file 369 std::fs::copy(&source_path, &target_path)?; 370 } 371 372 Ok(()) 373}