Author:
Hash:
Timestamp:
+97 -3 +/-6 browse
Kevin Schoon [me@kevinschoon.com]
0254c34753475465687cf89db4f986ea7199fddf
Tue, 15 Apr 2025 16:49:52 +0000 (1 month ago)
1 | diff --git a/src/axum/handlers_tag.rs b/src/axum/handlers_tag.rs |
2 | new file mode 100644 |
3 | index 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 |
36 | index 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 |
56 | index 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 |
77 | index 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 |
142 | index 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 |
155 | index 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 | } |