hub_tool/
repositories.rs

1use anyhow::Context;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::{fetch, DockerHubClient};
6
7#[derive(Serialize, Deserialize, Debug)]
8pub struct Category {
9    name: String,
10    slug: String,
11}
12
13#[derive(Serialize, Deserialize, Debug)]
14pub struct Repository {
15    /// The name of the repository on the Docker Hub
16    pub name: String,
17
18    /// The namespace i.e. user or organization where the repository lives in
19    namespace: String,
20
21    /// The type of repository, can be any of "image", etc.
22    repository_type: String,
23
24    status: usize,
25
26    status_description: String,
27
28    // TODO: It cannot be None, but it can be empty which is practically the same, so let's handle
29    // this in the future to have some consistency and use None() over Some("")
30    description: String,
31
32    is_private: bool,
33
34    star_count: usize,
35
36    pull_count: usize,
37
38    last_updated: DateTime<Utc>,
39
40    last_modified: DateTime<Utc>,
41
42    date_registered: DateTime<Utc>,
43
44    // TODO: same as in `description`
45    affiliation: String,
46
47    media_types: Vec<String>,
48
49    content_types: Vec<String>,
50
51    categories: Vec<Category>,
52
53    /// The size of the virtual image in bytes
54    storage_size: u64,
55}
56
57impl DockerHubClient {
58    /// List all the repositories under a given org or username on the Docker Hub
59    ///
60    /// This method lists all the repositories for a given organization or user via
61    /// the `org` argument that are uploaded and publicly available on the Docker Hub.
62    /// Note that if the repository is private but the provided token has access to it,
63    /// then the repositories will be listed, otherwise only the public ones (if any)
64    /// will be listed.
65    pub async fn list_repositories(&self, org: &str) -> anyhow::Result<Vec<Repository>> {
66        let url = self
67            .url
68            .join(&format!("v2/namespaces/{}/repositories", org)) // For some reason the endpoint `v2/repositories/{}` works seamlessly
69            .context("failed formatting the url with the provided org")?;
70
71        fetch::<Repository>(&self.client, &url)
72            .await
73            .context("fetching the provided url failed")
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use serde_json::json;
81
82    #[test]
83    fn test_repository_serde() {
84        let value = json!({
85          "name": "ollama",
86          "namespace": "ollama",
87          "repository_type": "image",
88          "status": 1,
89          "status_description": "active",
90          "description": "The easiest way to get up and running with large language models.",
91          "is_private": false,
92          "star_count": 1183,
93          "pull_count": 13256501,
94          "last_updated": "2025-03-04T04:01:22.754331Z",
95          "last_modified": "2024-10-16T13:48:34.145251Z",
96          "date_registered": "2023-06-29T23:27:34.326426Z",
97          "affiliation": "",
98          "media_types": [
99            "application/vnd.docker.container.image.v1+json",
100            "application/vnd.docker.distribution.manifest.list.v2+json",
101            "application/vnd.oci.image.config.v1+json",
102            "application/vnd.oci.image.index.v1+json"
103          ],
104          "content_types": [
105            "image"
106          ],
107          "categories": [
108            {
109              "name": "Machine Learning & AI",
110              "slug": "machine-learning-and-ai"
111            },
112            {
113              "name": "Developer Tools",
114              "slug": "developer-tools"
115            }
116          ],
117          "storage_size": 662988133055 as u64,
118        });
119
120        let repository = serde_json::from_value::<Repository>(value)
121            .context("failed to deserialize the repository payload")
122            .unwrap();
123
124        println!("{repository:#?}");
125    }
126
127    #[tokio::test]
128    async fn test_list_repositories() -> anyhow::Result<()> {
129        let pat =
130            std::env::var("DOCKER_PAT").context("environment variable `DOCKER_PAT` is not set")?;
131        let dh =
132            DockerHubClient::new(&pat).context("the docker hub client couldn't be instantiated")?;
133
134        println!("{:#?}", dh.list_repositories("ollama").await);
135
136        Ok(())
137    }
138}