1use 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#[derive(Debug, Clone)]
15pub struct CosClient {
16 config: Config,
17 auth: Auth,
18 http_client: Client,
19}
20
21impl CosClient {
22 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 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 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 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 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 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 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, ¶ms)?;
82 let mut headers = self.build_headers(&method, path, ¶ms)?;
83
84 let mut request_builder = self.http_client.request(method.clone(), &url);
86
87 for (key, value) in headers.iter() {
89 request_builder = request_builder.header(key, value);
90 }
91
92 if let Some(body) = body {
94 request_builder = request_builder.body(body);
95 }
96
97 let response = request_builder
99 .send()
100 .await
101 .map_err(|e| CosError::other(format!("Request failed: {}", e)))?;
102
103 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 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 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 headers.insert("User-Agent".to_string(), crate::USER_AGENT.to_string());
154 headers.insert("Host".to_string(), self.get_host(path)?);
155
156 let now = Utc::now();
158 let start_time = now - Duration::minutes(5); let end_time = now + Duration::hours(1); 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 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 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 serde_json::from_str(&text)
199 .map_err(|e| CosError::other(format!("Failed to parse XML response: {}", e)))
200 }
201
202 pub fn config(&self) -> &Config {
204 &self.config
205 }
206}
207
208mod 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", ¶ms).unwrap();
238 assert!(url.contains("test-bucket-123.cos.ap-beijing.myqcloud.com"));
239 assert!(url.contains("key=value"));
240 }
241}