Commit

Author:

Hash:

Timestamp:

+97 -3 +/-6 browse

Kevin Schoon [me@kevinschoon.com]

0254c34753475465687cf89db4f986ea7199fddf

Tue, 15 Apr 2025 16:49:52 +0000 (1 month ago)

begin implementing discovery with tag listing
1diff --git a/src/axum/handlers_tag.rs b/src/axum/handlers_tag.rs
2new file mode 100644
3index 0000000..857b88f
4--- /dev/null
5+++ b/src/axum/handlers_tag.rs
6 @@ -0,0 +1,28 @@
7+ use std::sync::Arc;
8+
9+ use axum::{
10+ Extension, Json,
11+ extract::{Query, State},
12+ response::{IntoResponse, Response},
13+ };
14+ use serde::Deserialize;
15+
16+ use crate::Namespace;
17+
18+ use super::{AppState, error::Error};
19+
20+ #[derive(Deserialize)]
21+ pub struct ListQuery {
22+ pub n: Option<u64>,
23+ pub last: Option<String>,
24+ }
25+
26+ pub async fn list(
27+ Extension(namespace): Extension<Namespace>,
28+ Query(query): Query<ListQuery>,
29+ State(state): State<Arc<AppState>>,
30+ ) -> Result<Response, Error> {
31+ let tags = state.oci.list_tags(&namespace, query.n, query.last).await?;
32+ let res = Json(tags).into_response();
33+ Ok(res)
34+ }
35 diff --git a/src/axum/mod.rs b/src/axum/mod.rs
36index 3522341..1b3fa14 100644
37--- a/src/axum/mod.rs
38+++ b/src/axum/mod.rs
39 @@ -18,6 +18,7 @@ mod error;
40 mod extractors;
41 mod handlers_blob;
42 mod handlers_manifest;
43+ mod handlers_tag;
44 mod paths;
45
46 #[derive(Clone)]
47 @@ -101,6 +102,7 @@ pub fn router(storage: &Storage) -> Router {
48 )
49 .route("/manifests/{digest_or_tag}", get(handlers_manifest::read))
50 .route("/manifests/{digest_or_tag}", head(handlers_manifest::stat))
51+ .route("/tags/list", get(handlers_tag::list))
52 // // .route("/{name}/blobs/{digest}", head(crate::handlers::stat_blob))
53 // // .route(
54 // // "/{name}/manifests/{reference}",
55 diff --git a/src/lib.rs b/src/lib.rs
56index 11acdaa..22a5984 100644
57--- a/src/lib.rs
58+++ b/src/lib.rs
59 @@ -1,4 +1,4 @@
60- use std::iter::{FromIterator, Iterator};
61+ use std::iter::Iterator;
62 use std::{fmt::Display, str::FromStr};
63
64 use error::Error;
65 @@ -36,6 +36,10 @@ impl Namespace {
66 pub fn path(&self) -> RelativePathBuf {
67 RelativePath::new(&self.0.join(SEPARATOR)).to_relative_path_buf()
68 }
69+
70+ pub fn name(&self) -> String {
71+ self.0.last().cloned().unwrap()
72+ }
73 }
74
75 impl TryFrom<&[&str]> for Namespace {
76 diff --git a/src/oci_interface.rs b/src/oci_interface.rs
77index 4966940..d9908d1 100644
78--- a/src/oci_interface.rs
79+++ b/src/oci_interface.rs
80 @@ -2,7 +2,10 @@ use std::{pin::Pin, str::FromStr, sync::Arc};
81
82 use bytes::Bytes;
83 use futures::{Stream, StreamExt};
84- use oci_spec::image::{Digest, ImageManifest};
85+ use oci_spec::{
86+ distribution::{TagList, TagListBuilder},
87+ image::{Digest, ImageManifest},
88+ };
89 use sha2::{Digest as HashDigest, Sha256};
90 use uuid::Uuid;
91
92 @@ -309,4 +312,48 @@ impl OciInterface {
93 };
94 self.storage.read(&blob_addr).await.map_err(Error::Storage)
95 }
96+
97+ pub async fn list_tags(
98+ &self,
99+ namespace: &Namespace,
100+ limit: Option<u64>,
101+ last: Option<String>,
102+ ) -> Result<TagList, Error> {
103+ let tag_dir = Address::TagDirectory {
104+ namespace: namespace.clone(),
105+ };
106+ if limit.is_some_and(|limit| limit == 0) && self.storage.exists(&tag_dir).await.is_ok() {
107+ return Ok(TagListBuilder::default()
108+ .name(namespace.name())
109+ .tags(Vec::new())
110+ .build()
111+ .unwrap());
112+ }
113+ let items = self.storage.list(&tag_dir).await.map_err(Error::Storage)?;
114+ let items = if let Some(tag_name) = last {
115+ if let Some(last_pos) = items
116+ .iter()
117+ .enumerate()
118+ .find_map(|(i, name)| if **name == tag_name { Some(i) } else { None })
119+ {
120+ items.split_at(last_pos+1).1
121+ } else {
122+ items.as_slice()
123+ }
124+ } else {
125+ items.as_slice()
126+ };
127+ if let Some(limit) = limit {
128+ let mut items = items.to_vec();
129+ items.truncate(limit as usize);
130+ Ok(TagListBuilder::default()
131+ .tags(items)
132+ .name(namespace.name())
133+ .build()
134+ .unwrap())
135+ } else {
136+ let tag_list = TagListBuilder::default().tags(items).name(namespace.name());
137+ Ok(tag_list.build().unwrap())
138+ }
139+ }
140 }
141 diff --git a/src/storage.rs b/src/storage.rs
142index 2c62c19..211d04f 100644
143--- a/src/storage.rs
144+++ b/src/storage.rs
145 @@ -42,7 +42,7 @@ impl Stream for InnerStream {
146 #[async_trait::async_trait]
147 pub trait StorageIface: Sync + Send {
148 /// List a single directory of objects
149- // async fn list(&self, addr: &Address) -> Result<Vec<Object>, Error>;
150+ async fn list(&self, addr: &Address) -> Result<Vec<String>, Error>;
151 // async fn stat(&self, addr: &Address) -> Result<Option<Object>, Error>;
152 // async fn read_bytes(&self, addr: &Address) -> Result<Option<Vec<u8>>, Error>;
153 /// Check if an object exists at the given address
154 diff --git a/src/storage_fs.rs b/src/storage_fs.rs
155index 19895f2..fac64a4 100644
156--- a/src/storage_fs.rs
157+++ b/src/storage_fs.rs
158 @@ -113,4 +113,17 @@ impl StorageIface for FileSystem {
159 tokio::fs::remove_file(path.as_path()).await?;
160 Ok(())
161 }
162+
163+ async fn list(&self, src: &Address) -> Result<Vec<String>, Error> {
164+ let path = src.path(&self.base);
165+ let mut items = tokio::fs::read_dir(path.as_path()).await?;
166+ let mut names: Vec<String> = Vec::new();
167+ while let Some(item) = items.next_entry().await? {
168+ let path = item.path();
169+ let name = path.file_name().unwrap().to_string_lossy();
170+ names.push(name.to_string())
171+ }
172+ names.sort();
173+ Ok(names)
174+ }
175 }