diff --git a/src/storage.rs b/src/storage.rs index 55af9c7..eff4f87 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -150,3 +150,34 @@ pub(crate) fn manifest_exists(org: &str, repo: &str, reference: &str) -> bool { ); std::path::Path::new(&manifest_path).exists() } + +pub(crate) fn list_tags(org: &str, repo: &str) -> Result, std::io::Error> { + let sanitized_org = sanitize_string(org); + let sanitized_repo = sanitize_string(repo); + + let manifests_dir = format!("./tmp/manifests/{}/{}", sanitized_org, sanitized_repo); + let path = std::path::Path::new(&manifests_dir); + + if !path.exists() { + return Ok(Vec::new()); + } + + let mut tags = Vec::new(); + + for entry in std::fs::read_dir(path)? { + let entry = entry?; + if entry.path().is_file() { + if let Some(filename) = entry.file_name().to_str() { + // Filter out digest references (start with sha256:) + // Only include tag names + if !filename.starts_with("sha256:") { + tags.push(filename.to_string()); + } + } + } + } + + // Sort tags alphabetically for consistent ordering + tags.sort(); + Ok(tags) +} diff --git a/src/tags.rs b/src/tags.rs index 65ea5b3..48e1700 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -3,36 +3,98 @@ // | end-8a | `GET` | `/v2//tags/list` | `200` | `404` | // | end-8b | `GET` | `/v2//tags/list?n=&last=` | `200` | `404` | +use axum::body::Body; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::Response; use serde::Deserialize; -use serde_json::{json, Value}; use std::sync::Arc; -use crate::state; -use axum::{ - extract::{Path, Query, State}, - response::Json, -}; +use crate::{auth, state, storage}; +use axum::extract::{Path, Query, State}; // end-8a GET /v2/:name/tags/list // end-8b GET /v2/:name/tags/list?n=&last= #[derive(Deserialize)] -pub(crate) struct End8bQueryParams { - n: String, - last: String, +pub(crate) struct TagsQuery { + pub n: Option, + pub last: Option, } + +fn paginate_tags(tags: Vec, n: Option, last: Option) -> Vec { + let mut result = tags; + + // Filter tags after 'last' cursor + if let Some(last_tag) = last { + result = result + .into_iter() + .skip_while(|tag| tag <= &last_tag) + .collect(); + } + + // Limit to 'n' results + if let Some(limit) = n { + result.truncate(limit); + } + + result +} + pub(crate) async fn get_tags_list( - State(data): State>, - Path(name): Path, - query: Query, -) -> Json { - let status = data.server_status.lock().await; - log::info!( - "tags/get_tags_list: name: {}, n: {}, last: {}", - name, - query.n, - query.last - ); - Json(json!({ - "not_implemented": format!("name {} n {:?} last {:?} server_status {}", name, query.n, query.last, status) - })) + State(state): State>, + Path((org, repo)): Path<(String, String)>, + Query(params): Query, + headers: HeaderMap, +) -> Response { + let host = &state.args.host; + + // Authenticate + if auth::get(State(state.clone()), headers.clone()) + .await + .status() + != StatusCode::OK + { + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header( + "WWW-Authenticate", + format!("Basic realm=\"{}\", charset=\"UTF-8\"", host), + ) + .body(Body::from("401 Unauthorized")) + .unwrap(); + } + + // Get all tags from storage + match storage::list_tags(&org, &repo) { + Ok(all_tags) => { + // Apply pagination + let paginated_tags = paginate_tags(all_tags, params.n, params.last); + + // Build response JSON + let response_body = serde_json::json!({ + "name": format!("{}/{}", org, repo), + "tags": paginated_tags + }); + + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(Body::from(response_body.to_string())) + .unwrap() + } + Err(e) => { + log::error!("Failed to list tags for {}/{}: {}", org, repo, e); + + // Return empty list if directory doesn't exist (valid case) + let response_body = serde_json::json!({ + "name": format!("{}/{}", org, repo), + "tags": [] + }); + + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(Body::from(response_body.to_string())) + .unwrap() + } + } }