cos_rust_sdk/
client.rs

1//! HTTP 客户端模块
2//!
3//! 提供与腾讯云 COS 服务通信的 HTTP 客户端功能
4
5use crate::auth::Auth;
6use crate::config::Config;
7use crate::error::{CosError, Result};
8use chrono::{Duration, Utc};
9use reqwest::{Client, Method, Response};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// COS HTTP 客户端
14#[derive(Debug, Clone)]
15pub struct CosClient {
16    config: Config,
17    auth: Auth,
18    http_client: Client,
19}
20
21impl CosClient {
22    /// 创建新的 COS 客户端
23    pub fn new(config: Config) -> Result<Self> {
24        config.validate()?;
25        
26        let auth = Auth::new(&config.secret_id, &config.secret_key);
27        let http_client = Client::builder()
28            .timeout(config.timeout)
29            .build()
30            .map_err(|e| CosError::other(format!("Failed to create HTTP client: {}", e)))?;
31
32        Ok(Self {
33            config,
34            auth,
35            http_client,
36        })
37    }
38
39    /// 发送 GET 请求
40    pub async fn get(&self, path: &str, params: HashMap<String, String>) -> Result<Response> {
41        self.request(Method::GET, path, params, None::<&[u8]>).await
42    }
43
44    /// 发送 PUT 请求
45    pub async fn put<T>(&self, path: &str, params: HashMap<String, String>, body: Option<T>) -> Result<Response>
46    where
47        T: Into<reqwest::Body>,
48    {
49        self.request(Method::PUT, path, params, body).await
50    }
51
52    /// 发送 POST 请求
53    pub async fn post<T>(&self, path: &str, params: HashMap<String, String>, body: Option<T>) -> Result<Response>
54    where
55        T: Into<reqwest::Body>,
56    {
57        self.request(Method::POST, path, params, body).await
58    }
59
60    /// 发送 DELETE 请求
61    pub async fn delete(&self, path: &str, params: HashMap<String, String>) -> Result<Response> {
62        self.request(Method::DELETE, path, params, None::<&[u8]>).await
63    }
64
65    /// 发送 HEAD 请求
66    pub async fn head(&self, path: &str, params: HashMap<String, String>) -> Result<Response> {
67        self.request(Method::HEAD, path, params, None::<&[u8]>).await
68    }
69
70    /// 通用请求方法
71    async fn request<T>(
72        &self,
73        method: Method,
74        path: &str,
75        params: HashMap<String, String>,
76        body: Option<T>,
77    ) -> Result<Response>
78    where
79        T: Into<reqwest::Body>,
80    {
81        let url = self.build_url(path, &params)?;
82        let mut headers = self.build_headers(&method, path, &params)?;
83        
84        // 构建请求
85        let mut request_builder = self.http_client.request(method.clone(), &url);
86        
87        // 添加请求头
88        for (key, value) in headers.iter() {
89            request_builder = request_builder.header(key, value);
90        }
91        
92        // 添加请求体
93        if let Some(body) = body {
94            request_builder = request_builder.body(body);
95        }
96        
97        // 发送请求
98        let response = request_builder
99            .send()
100            .await
101            .map_err(|e| CosError::other(format!("Request failed: {}", e)))?;
102        
103        // 检查响应状态
104        if !response.status().is_success() {
105            let status = response.status();
106            let error_text = response
107                .text()
108                .await
109                .unwrap_or_else(|_| "Unknown error".to_string());
110            
111            return Err(CosError::server(
112                status.to_string(),
113                error_text
114            ));
115        }
116        
117        Ok(response)
118    }
119
120    /// 构建完整的 URL
121    fn build_url(&self, path: &str, params: &HashMap<String, String>) -> Result<String> {
122        let base_url = if path.starts_with('/') {
123            self.config.bucket_url()?
124        } else {
125            self.config.service_url()
126        };
127        
128        let mut url = format!("{}{}", base_url, path);
129        
130        if !params.is_empty() {
131            let query_string = params
132                .iter()
133                .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
134                .collect::<Vec<_>>()
135                .join("&");
136            url.push('?');
137            url.push_str(&query_string);
138        }
139        
140        Ok(url)
141    }
142
143    /// 构建请求头
144    fn build_headers(
145        &self,
146        method: &Method,
147        path: &str,
148        params: &HashMap<String, String>,
149    ) -> Result<HashMap<String, String>> {
150        let mut headers = HashMap::new();
151        
152        // 基础请求头
153        headers.insert("User-Agent".to_string(), crate::USER_AGENT.to_string());
154        headers.insert("Host".to_string(), self.get_host(path)?);
155        
156        // 时间相关
157        let now = Utc::now();
158        let start_time = now - Duration::minutes(5); // 提前5分钟
159        let end_time = now + Duration::hours(1);     // 1小时后过期
160        
161        // 生成授权签名
162        let authorization = self.auth.sign(
163            method.as_str(),
164            path,
165            &headers,
166            params,
167            start_time,
168            end_time,
169        )?;
170        
171        headers.insert("Authorization".to_string(), authorization);
172        
173        Ok(headers)
174    }
175
176    /// 获取主机名
177    fn get_host(&self, path: &str) -> Result<String> {
178        let url = if path.starts_with('/') {
179            self.config.bucket_url()?
180        } else {
181            self.config.service_url()
182        };
183        
184        let parsed_url = url::Url::parse(&url)
185            .map_err(|e| CosError::other(format!("Invalid URL: {}", e)))?;
186        
187        Ok(parsed_url.host_str().unwrap_or("localhost").to_string())
188    }
189
190    /// 解析 XML 响应
191    pub async fn parse_xml_response(response: Response) -> Result<Value> {
192        let text = response
193            .text()
194            .await
195            .map_err(|e| CosError::other(format!("Failed to read response: {}", e)))?;
196        
197        // 简单的 XML 到 JSON 转换(实际项目中可能需要更复杂的解析)
198        serde_json::from_str(&text)
199            .map_err(|e| CosError::other(format!("Failed to parse XML response: {}", e)))
200    }
201
202    /// 获取配置
203    pub fn config(&self) -> &Config {
204        &self.config
205    }
206}
207
208/// URL 编码工具
209mod urlencoding {
210    pub fn encode(input: &str) -> String {
211        url::form_urlencoded::byte_serialize(input.as_bytes()).collect()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use std::time::Duration as StdDuration;
219
220    #[test]
221    fn test_client_creation() {
222        let config = Config::new("test_id", "test_key", "ap-beijing", "test-bucket-123")
223            .with_timeout(StdDuration::from_secs(60));
224        
225        let client = CosClient::new(config);
226        assert!(client.is_ok());
227    }
228
229    #[test]
230    fn test_build_url() {
231        let config = Config::new("test_id", "test_key", "ap-beijing", "test-bucket-123");
232        let client = CosClient::new(config).unwrap();
233        
234        let mut params = HashMap::new();
235        params.insert("key".to_string(), "value".to_string());
236        
237        let url = client.build_url("/test", &params).unwrap();
238        assert!(url.contains("test-bucket-123.cos.ap-beijing.myqcloud.com"));
239        assert!(url.contains("key=value"));
240    }
241}