1use anyhow::Context;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::{fetch, DockerHubClient};
6
7#[derive(Serialize, Deserialize, Debug)]
8pub struct Image {
9 architecture: String,
10 features: String,
11 variant: Option<String>,
12 digest: String,
13 os: Option<String>,
14 os_features: String,
15 os_version: Option<String>,
16 size: u64,
17 status: String,
18 last_pulled: DateTime<Utc>,
19 last_pushed: DateTime<Utc>,
20}
21
22#[derive(Serialize, Deserialize, Debug)]
23pub struct Tag {
24 creator: u64,
26
27 id: u64,
29
30 images: Vec<Image>,
31 last_updated: DateTime<Utc>,
32 last_updater: u64,
33 last_updater_username: String,
34
35 name: String,
37
38 repository: u64,
39 full_size: u64,
40 v2: bool,
41 tag_status: String,
42 tag_last_pulled: DateTime<Utc>,
43 tag_last_pushed: DateTime<Utc>,
44 media_type: String,
45 content_type: String,
46 digest: String,
47}
48
49impl DockerHubClient {
50 pub async fn list_tags(&self, org: &str, repository: &str) -> anyhow::Result<Vec<Tag>> {
56 let url = self
57 .url
58 .join(&format!(
59 "v2/namespaces/{}/repositories/{}/tags", org, repository
61 ))
62 .context("failed formatting the url with the provided org and repository")?;
63
64 fetch::<Tag>(&self.client, &url)
65 .await
66 .context("fetching the provided url failed")
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use serde_json::json;
74
75 #[test]
76 fn test_tag_serde() {
77 let value = json!({
78 "creator": 14304909,
79 "id": 529481097,
80 "images": [
81 {
82 "architecture": "amd64",
83 "features": "",
84 "variant": null,
85 "digest": "sha256:96b6a4e66250499a9d87a4adf259ced7cd213e2320fb475914217f4d69abe98d",
86 "os": "linux",
87 "os_features": "",
88 "os_version": null,
89 "size": 755930694,
90 "status": "active",
91 "last_pulled": "2025-03-05T07:52:00.613197154Z",
92 "last_pushed": "2024-01-16T20:54:52Z"
93 }
94 ],
95 "last_updated": "2024-01-16T20:54:55.914808Z",
96 "last_updater": 14304909,
97 "last_updater_username": "mxyng",
98 "name": "gguf",
99 "repository": 22180121,
100 "full_size": 755930694,
101 "v2": true,
102 "tag_status": "active",
103 "tag_last_pulled": "2025-03-05T07:52:00.613197154Z",
104 "tag_last_pushed": "2024-01-16T20:54:55.914808Z",
105 "media_type": "application/vnd.oci.image.index.v1+json",
106 "content_type": "image",
107 "digest": "sha256:7c49490a9e4a7ca4326e09c4b47bc525aa0a9dfc8ea0b3a30d62af23a60db712"
108 });
109
110 let tag = serde_json::from_value::<Tag>(value)
111 .context("failed to deserialize the tag payload")
112 .unwrap();
113
114 println!("{tag:#?}");
115 }
116
117 #[tokio::test]
118 async fn test_list_tags() -> anyhow::Result<()> {
119 let pat =
120 std::env::var("DOCKER_PAT").context("environment variable `DOCKER_PAT` is not set")?;
121 let dh =
122 DockerHubClient::new(&pat).context("the docker hub client couldn't be instantiated")?;
123
124 println!("{:#?}", dh.list_tags("ollama", "ollama").await);
125
126 Ok(())
127 }
128}