Skip to main content

docker_registry/
client.rs

1#![warn(missing_docs)]
2
3//! 镜像仓库 API 客户端
4//!
5//! 提供与镜像仓库 API 交互的客户端实现,支持 Docker Hub 和其他仓库类型。
6
7use std::sync::Arc;
8use std::time::Duration;
9
10use wae_request::{HttpClient, HttpClientConfig, HttpResponse, RequestBuilder};
11
12use crate::types::{AuthResponse, ImageManifest};
13use docker_types::{DockerError, Result};
14
15/// 镜像仓库客户端 trait
16///
17/// 定义了与镜像仓库交互的核心方法,包括获取镜像 manifest 和下载镜像层。
18pub trait RegistryClient {
19    /// 获取镜像 Manifest
20    ///
21    /// # 参数
22    /// - `image`: 镜像名称
23    /// - `tag`: 镜像标签
24    ///
25    /// # 返回
26    /// - `Result<ImageManifest>`: 成功返回镜像 manifest,失败返回错误
27    async fn get_manifest(&self, image: &str, tag: &str) -> Result<ImageManifest>;
28
29    /// 下载镜像层
30    ///
31    /// # 参数
32    /// - `image`: 镜像名称
33    /// - `digest`: 镜像层摘要
34    ///
35    /// # 返回
36    /// - `Result<HttpResponse>`: 成功返回 HTTP 响应,失败返回错误
37    async fn download_layer(&self, image: &str, digest: &str) -> Result<HttpResponse>;
38
39    /// 下载镜像层(支持续点续传)
40    ///
41    /// # 参数
42    /// - `image`: 镜像名称
43    /// - `digest`: 镜像层摘要
44    /// - `start`: 开始下载的位置
45    ///
46    /// # 返回
47    /// - `Result<HttpResponse>`: 成功返回 HTTP 响应,失败返回错误
48    async fn download_layer_with_range(
49        &self,
50        image: &str,
51        digest: &str,
52        start: u64,
53    ) -> Result<HttpResponse>;
54
55    /// 获取基本 URL
56    ///
57    /// # 返回
58    /// - `&str`: 仓库的基本 URL
59    fn get_base_url(&self) -> &str;
60}
61
62/// Docker Hub 客户端
63///
64/// 实现了与 Docker Hub 仓库交互的客户端,支持获取镜像 manifest 和下载镜像层。
65pub struct DockerHubClient {
66    /// HTTP 客户端,用于发送 API 请求
67    client: HttpClient,
68    /// Docker Hub 基本 URL
69    base_url: String,
70}
71
72impl DockerHubClient {
73    /// 创建新的 Docker Hub 客户端
74    ///
75    /// # 返回
76    /// - `Result<Self>`: 成功返回客户端实例,失败返回错误
77    pub fn new() -> Result<Self> {
78        let config = HttpClientConfig {
79            timeout: Duration::from_secs(30),
80            connect_timeout: Duration::from_secs(10),
81            ..Default::default()
82        };
83
84        let client = HttpClient::new(config);
85
86        Ok(Self {
87            client,
88            base_url: "https://registry-1.docker.io".to_string(),
89        })
90    }
91
92    /// 获取镜像 Manifest(内部实现)
93    ///
94    /// # 参数
95    /// - `image`: 镜像名称
96    /// - `tag`: 镜像标签
97    ///
98    /// # 返回
99    /// - `Result<ImageManifest>`: 成功返回镜像 manifest,失败返回错误
100    async fn get_manifest_internal(&self, image: &str, tag: &str) -> Result<ImageManifest> {
101        let url = format!("{}/v2/{}/manifests/{}", self.base_url, image, tag);
102
103        // 重试逻辑
104        for attempt in 0..3 {
105            let response = match self
106                .client
107                .get_with_headers(
108                    &url,
109                    [(
110                        "Accept".to_string(),
111                        "application/vnd.docker.distribution.manifest.v2+json".to_string(),
112                    )]
113                    .into(),
114                )
115                .await
116            {
117                Ok(response) => response,
118                Err(e) => {
119                    if attempt < 2 {
120                        tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
121                        continue;
122                    }
123                    return Err(DockerError::request_error(&url, e.to_string()).into());
124                }
125            };
126
127            if !response.is_success() {
128                // 如果需要认证,获取认证令牌
129                if response.status == 401 {
130                    if let Some(www_auth) = response.headers.get("www-authenticate") {
131                        let auth_header = www_auth;
132                        let token = self.get_auth_token(auth_header, image).await?;
133
134                        // 使用令牌重新请求
135                        let response = self
136                            .client
137                            .get_with_headers(
138                                &url,
139                                [
140                                    (
141                                        "Accept".to_string(),
142                                        "application/vnd.docker.distribution.manifest.v2+json"
143                                            .to_string(),
144                                    ),
145                                    ("Authorization".to_string(), format!("Bearer {}", token)),
146                                ]
147                                .into(),
148                            )
149                            .await
150                            .map_err(|e| DockerError::request_error(&url, e.to_string()))?;
151
152                        let manifest = response
153                            .json::<ImageManifest>()
154                            .map_err(|e| DockerError::request_error(&url, e.to_string()))?;
155                        return Ok(manifest);
156                    }
157                }
158                return Err(DockerError::registry_error(format!(
159                    "Failed to get manifest: {}",
160                    response.status
161                ))
162                .into());
163            }
164
165            let manifest = response
166                .json::<ImageManifest>()
167                .map_err(|e| DockerError::json_error(e.to_string()))?;
168            return Ok(manifest);
169        }
170
171        Err(DockerError::registry_error(
172            "Failed to get manifest after multiple attempts".to_string(),
173        )
174        .into())
175    }
176
177    /// 获取认证令牌
178    ///
179    /// # 参数
180    /// - `auth_header`: WWW-Authenticate 头信息
181    /// - `image`: 镜像名称
182    ///
183    /// # 返回
184    /// - `Result<String>`: 成功返回认证令牌,失败返回错误
185    async fn get_auth_token(&self, auth_header: &str, image: &str) -> Result<String> {
186        // 解析 WWW-Authenticate 头
187        let parts: Vec<&str> = auth_header.split(' ').collect();
188        if parts.len() != 2 || parts[0] != "Bearer" {
189            return Err(
190                DockerError::registry_error("Invalid WWW-Authenticate header".to_string()).into(),
191            );
192        }
193
194        let params: Vec<&str> = parts[1].split(',').collect();
195        let mut realm = "";
196        let mut service = "";
197        let mut scope = "";
198
199        for param in params {
200            let param_parts: Vec<&str> = param.split('=').collect();
201            if param_parts.len() == 2 {
202                let key = param_parts[0].trim();
203                let value = param_parts[1].trim().trim_matches('"');
204                match key {
205                    "realm" => realm = value,
206                    "service" => service = value,
207                    "scope" => scope = value,
208                    _ => {}
209                }
210            }
211        }
212
213        if realm.is_empty() || service.is_empty() {
214            return Err(
215                DockerError::registry_error("Invalid WWW-Authenticate header".to_string()).into(),
216            );
217        }
218
219        let scope = if scope.is_empty() {
220            format!("repository:{}/pull", image)
221        } else {
222            scope.to_string()
223        };
224
225        let auth_url = format!("{}?service={}&scope={}", realm, service, scope);
226
227        let response = self
228            .client
229            .get(&auth_url)
230            .await
231            .map_err(|e| DockerError::request_error(&auth_url, e.to_string()))?;
232
233        if !response.is_success() {
234            return Err(DockerError::registry_error(format!(
235                "Failed to get auth token: {}",
236                response.status
237            ))
238            .into());
239        }
240
241        let auth_response = response
242            .json::<AuthResponse>()
243            .map_err(|e| DockerError::json_error(e.to_string()))?;
244        Ok(auth_response.token)
245    }
246
247    /// 下载镜像层(内部实现)
248    ///
249    /// # 参数
250    /// - `image`: 镜像名称
251    /// - `digest`: 镜像层摘要
252    ///
253    /// # 返回
254    /// - `Result<HttpResponse>`: 成功返回 HTTP 响应,失败返回错误
255    async fn download_layer_internal(&self, image: &str, digest: &str) -> Result<HttpResponse> {
256        let url = format!("{}/v2/{}/blobs/{}", self.base_url, image, digest);
257
258        // 重试逻辑
259        for attempt in 0..3 {
260            let response = match self.client.get(&url).await {
261                Ok(response) => response,
262                Err(e) => {
263                    if attempt < 2 {
264                        tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
265                        continue;
266                    }
267                    return Err(DockerError::request_error(&url, e.to_string()).into());
268                }
269            };
270
271            if !response.is_success() {
272                // 如果需要认证,获取认证令牌
273                if response.status == 401 {
274                    if let Some(www_auth) = response.headers.get("www-authenticate") {
275                        let auth_header = www_auth;
276                        let token = self.get_auth_token(auth_header, image).await?;
277
278                        // 使用令牌重新请求
279                        let response = self
280                            .client
281                            .get_with_headers(
282                                &url,
283                                [("Authorization".to_string(), format!("Bearer {}", token))].into(),
284                            )
285                            .await
286                            .map_err(|e| DockerError::request_error(&url, e.to_string()))?;
287
288                        return Ok(response);
289                    }
290                }
291                return Err(DockerError::registry_error(format!(
292                    "Failed to download layer: {}",
293                    response.status
294                ))
295                .into());
296            }
297
298            return Ok(response);
299        }
300
301        Err(DockerError::registry_error(
302            "Failed to download layer after multiple attempts".to_string(),
303        )
304        .into())
305    }
306
307    /// 下载镜像层(支持续点续传,内部实现)
308    ///
309    /// # 参数
310    /// - `image`: 镜像名称
311    /// - `digest`: 镜像层摘要
312    /// - `start`: 开始下载的位置
313    ///
314    /// # 返回
315    /// - `Result<HttpResponse>`: 成功返回 HTTP 响应,失败返回错误
316    async fn download_layer_with_range_internal(
317        &self,
318        image: &str,
319        digest: &str,
320        start: u64,
321    ) -> Result<HttpResponse> {
322        let url = format!("{}/v2/{}/blobs/{}", self.base_url, image, digest);
323
324        // 重试逻辑
325        for attempt in 0..3 {
326            let response = match self
327                .client
328                .get_with_headers(
329                    &url,
330                    [("Range".to_string(), format!("bytes={}-", start))].into(),
331                )
332                .await
333            {
334                Ok(response) => response,
335                Err(e) => {
336                    if attempt < 2 {
337                        tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
338                        continue;
339                    }
340                    return Err(DockerError::request_error(&url, e.to_string()).into());
341                }
342            };
343
344            if !response.is_success() && response.status != 206 {
345                // 如果需要认证,获取认证令牌
346                if response.status == 401 {
347                    if let Some(www_auth) = response.headers.get("www-authenticate") {
348                        let auth_header = www_auth;
349                        let token = self.get_auth_token(auth_header, image).await?;
350
351                        // 使用令牌重新请求
352                        let response = self
353                            .client
354                            .get_with_headers(
355                                &url,
356                                [
357                                    ("Range".to_string(), format!("bytes={}-", start)),
358                                    ("Authorization".to_string(), format!("Bearer {}", token)),
359                                ]
360                                .into(),
361                            )
362                            .await
363                            .map_err(|e| DockerError::request_error(&url, e.to_string()))?;
364
365                        return Ok(response);
366                    }
367                }
368                return Err(DockerError::registry_error(format!(
369                    "Failed to download layer: {}",
370                    response.status
371                ))
372                .into());
373            }
374
375            return Ok(response);
376        }
377
378        Err(DockerError::registry_error(
379            "Failed to download layer after multiple attempts".to_string(),
380        )
381        .into())
382    }
383}
384
385impl Default for DockerHubClient {
386    /// 创建默认的 Docker Hub 客户端
387    ///
388    /// # 注意
389    /// 如果创建失败,会直接 panic
390    fn default() -> Self {
391        Self::new().expect("Failed to create DockerHubClient")
392    }
393}
394
395impl RegistryClient for DockerHubClient {
396    async fn get_manifest(&self, image: &str, tag: &str) -> Result<ImageManifest> {
397        self.get_manifest_internal(image, tag).await
398    }
399
400    async fn download_layer(&self, image: &str, digest: &str) -> Result<HttpResponse> {
401        self.download_layer_internal(image, digest).await
402    }
403
404    async fn download_layer_with_range(
405        &self,
406        image: &str,
407        digest: &str,
408        start: u64,
409    ) -> Result<HttpResponse> {
410        self.download_layer_with_range_internal(image, digest, start)
411            .await
412    }
413
414    fn get_base_url(&self) -> &str {
415        &self.base_url
416    }
417}