Add _catalog endpoints for listing repositories

This commit is contained in:
SeanOMik 2023-04-18 00:23:03 -04:00
parent 16da8aa190
commit 82774a4931
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
5 changed files with 106 additions and 13 deletions

66
src/api/catalog.rs Normal file
View File

@ -0,0 +1,66 @@
use actix_web::{HttpResponse, web, get, HttpRequest};
use qstring::QString;
use serde::{Serialize, Deserialize};
use crate::{app_state::AppState, database::Database};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RepositoryList {
repositories: Vec<String>,
}
#[get("")]
pub async fn list_repositories(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
// Get limit and last tag from query params if present.
let qs = QString::from(req.query_string());
let limit = qs.get("n");
let last_repo = qs.get("last");
let mut link_header = None;
// Paginate tag results if n was specified, else just pull everything.
let database = &state.database;
let repositories = match limit {
Some(limit) => {
let limit: u32 = limit.parse().unwrap();
// Convert the last param to a String, and list all the repos
let last_repo = last_repo.and_then(|t| Some(t.to_string()));
let repos = database.list_repositories(Some(limit), last_repo).await.unwrap();
// Get the new last repository for the response
let last_repo = repos.last().and_then(|s| Some(s.clone()));
// Construct the link header
let url = req.uri().to_string();
let mut url = format!("<{}/v2/_catalog?n={}", url, limit);
if let Some(last_repo) = last_repo {
url += &format!("&limit={}", last_repo);
}
url += ">; rel=\"next\"";
link_header = Some(url);
repos
},
None => {
database.list_repositories(None, None).await.unwrap()
}
};
// Convert the `Vec<Tag>` to a `TagList` which will be serialized to json.
let repo_list = RepositoryList {
repositories,
};
let response_body = serde_json::to_string(&repo_list).unwrap();
// Construct the response, optionally adding the Link header if it was constructed.
let mut resp = HttpResponse::Ok();
resp.append_header(("Content-Type", "application/json"));
if let Some(link_header) = link_header {
resp.append_header(("Link", link_header));
}
resp.body(response_body)
}

View File

@ -2,6 +2,7 @@ pub mod blobs;
pub mod uploads; pub mod uploads;
pub mod manifests; pub mod manifests;
pub mod tags; pub mod tags;
pub mod catalog;
use actix_web::{HttpResponse, get}; use actix_web::{HttpResponse, get};

View File

@ -28,20 +28,25 @@ pub async fn list_tags(path: web::Path<(String, )>, req: HttpRequest, state: web
Some(limit) => { Some(limit) => {
let limit: u32 = limit.parse().unwrap(); let limit: u32 = limit.parse().unwrap();
// Convert the last param to a String, and list all the tags
let last_tag = last_tag.and_then(|t| Some(t.to_string())); let last_tag = last_tag.and_then(|t| Some(t.to_string()));
let tags = database.list_repository_tags_page(&name, limit, last_tag).await.unwrap();
// Get the new last repository for the response
let last_tag = tags.last();
// Construct the link header // Construct the link header
let mut url = format!("/v2/{}/tags/list?n={}", name, limit); let url = req.uri().to_string();
if let Some(last_tag) = last_tag.clone() { let mut url = format!("<{}/v2/{}/tags/list?n={}", url, name, limit);
url += &format!("&limit={}", last_tag); if let Some(last_tag) = last_tag {
url += &format!("&limit={}", last_tag.name);
} }
url += ";rel=\"next\""; url += ">; rel=\"next\"";
link_header = Some(url); link_header = Some(url);
database.list_repository_tags_page(&name, limit, last_tag).await.unwrap() tags
}, },
None => { None => {
let database = &state.database;
database.list_repository_tags(&name).await.unwrap() database.list_repository_tags(&name).await.unwrap()
} }
}; };

View File

@ -54,8 +54,9 @@ pub trait Database {
/// Create a repository /// Create a repository
async fn save_repository(&self, repository: &str) -> sqlx::Result<()>; async fn save_repository(&self, repository: &str) -> sqlx::Result<()>;
/// List all repositories /// List all repositories.
async fn list_repositories(&self) -> sqlx::Result<Vec<String>>; /// If limit is not specified, a default limit of 1000 will be returned.
async fn list_repositories(&self, limit: Option<u32>, last_repo: Option<String>) -> sqlx::Result<Vec<String>>;
} }
#[async_trait] #[async_trait]
@ -285,11 +286,27 @@ impl Database for Pool<Sqlite> {
Ok(()) Ok(())
} }
async fn list_repositories(&self) -> sqlx::Result<Vec<String>> { //async fn list_repositories(&self) -> sqlx::Result<Vec<String>> {
let repos: Vec<(String, )> = sqlx::query_as("SELECT name FROM repositories") async fn list_repositories(&self, limit: Option<u32>, last_repo: Option<String>) -> sqlx::Result<Vec<String>> {
.fetch_all(self).await?; let limit = limit.unwrap_or(1000); // set default limit
// Move out of repos
let repos = repos.into_iter().map(|row| row.0).collect(); // Query differently depending on if `last_repo` was specified
let rows: Vec<(String, )> = match last_repo {
Some(last_repo) => {
sqlx::query_as("SELECT name FROM repositories WHERE name > ? ORDER BY name LIMIT ?")
.bind(last_repo)
.bind(limit)
.fetch_all(self).await?
},
None => {
sqlx::query_as("SELECT name FROM repositories ORDER BY name LIMIT ?")
.bind(limit)
.fetch_all(self).await?
}
};
// "unwrap" the tuple from the rows
let repos: Vec<String> = rows.into_iter().map(|row| row.0).collect();
Ok(repos) Ok(repos)
} }

View File

@ -40,6 +40,10 @@ async fn main() -> std::io::Result<()> {
.service( .service(
web::scope("/v2") web::scope("/v2")
.service(api::version_check) .service(api::version_check)
.service(
web::scope("/_catalog")
.service(api::catalog::list_repositories)
)
.service( .service(
web::scope("/{name}") web::scope("/{name}")
.service( .service(