Author:
Hash:
Timestamp:
+97 -3 +/-6 browse
Kevin Schoon [me@kevinschoon.com]
0254c34753475465687cf89db4f986ea7199fddf
Tue, 15 Apr 2025 16:49:52 +0000 (1.1 years 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 | } |