use crate::auth::Auth;
use crate::config::Config;
use crate::error::{CosError, Result};
use chrono::{Duration, Utc};
use reqwest::{Client, Method, Response};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct CosClient {
config: Config,
auth: Auth,
http_client: Client,
}
impl CosClient {
pub fn new(config: Config) -> Result<Self> {
config.validate()?;
let auth = Auth::new(&config.secret_id, &config.secret_key);
let http_client = Client::builder()
.timeout(config.timeout)
.build()
.map_err(|e| CosError::other(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
config,
auth,
http_client,
})
}
pub async fn get(&self, path: &str, params: HashMap<String, String>) -> Result<Response> {
self.request(Method::GET, path, params, None::<&[u8]>).await
}
pub async fn put<T>(&self, path: &str, params: HashMap<String, String>, body: Option<T>) -> Result<Response>
where
T: Into<reqwest::Body>,
{
self.request(Method::PUT, path, params, body).await
}
pub async fn post<T>(&self, path: &str, params: HashMap<String, String>, body: Option<T>) -> Result<Response>
where
T: Into<reqwest::Body>,
{
self.request(Method::POST, path, params, body).await
}
pub async fn delete(&self, path: &str, params: HashMap<String, String>) -> Result<Response> {
self.request(Method::DELETE, path, params, None::<&[u8]>).await
}
pub async fn head(&self, path: &str, params: HashMap<String, String>) -> Result<Response> {
self.request(Method::HEAD, path, params, None::<&[u8]>).await
}
async fn request<T>(
&self,
method: Method,
path: &str,
params: HashMap<String, String>,
body: Option<T>,
) -> Result<Response>
where
T: Into<reqwest::Body>,
{
let url = self.build_url(path, ¶ms)?;
let mut headers = self.build_headers(&method, path, ¶ms)?;
let mut request_builder = self.http_client.request(method.clone(), &url);
for (key, value) in headers.iter() {
request_builder = request_builder.header(key, value);
}
if let Some(body) = body {
request_builder = request_builder.body(body);
}
let response = request_builder
.send()
.await
.map_err(|e| CosError::other(format!("Request failed: {}", e)))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(CosError::server(
status.to_string(),
error_text
));
}
Ok(response)
}
fn build_url(&self, path: &str, params: &HashMap<String, String>) -> Result<String> {
let base_url = if path.starts_with('/') {
self.config.bucket_url()?
} else {
self.config.service_url()
};
let mut url = format!("{}{}", base_url, path);
if !params.is_empty() {
let query_string = params
.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
url.push('?');
url.push_str(&query_string);
}
Ok(url)
}
fn build_headers(
&self,
method: &Method,
path: &str,
params: &HashMap<String, String>,
) -> Result<HashMap<String, String>> {
let mut headers = HashMap::new();
headers.insert("User-Agent".to_string(), crate::USER_AGENT.to_string());
headers.insert("Host".to_string(), self.get_host(path)?);
let now = Utc::now();
let start_time = now - Duration::minutes(5); let end_time = now + Duration::hours(1);
let authorization = self.auth.sign(
method.as_str(),
path,
&headers,
params,
start_time,
end_time,
)?;
headers.insert("Authorization".to_string(), authorization);
Ok(headers)
}
fn get_host(&self, path: &str) -> Result<String> {
let url = if path.starts_with('/') {
self.config.bucket_url()?
} else {
self.config.service_url()
};
let parsed_url = url::Url::parse(&url)
.map_err(|e| CosError::other(format!("Invalid URL: {}", e)))?;
Ok(parsed_url.host_str().unwrap_or("localhost").to_string())
}
pub async fn parse_xml_response(response: Response) -> Result<Value> {
let text = response
.text()
.await
.map_err(|e| CosError::other(format!("Failed to read response: {}", e)))?;
serde_json::from_str(&text)
.map_err(|e| CosError::other(format!("Failed to parse XML response: {}", e)))
}
pub fn config(&self) -> &Config {
&self.config
}
}
mod urlencoding {
pub fn encode(input: &str) -> String {
url::form_urlencoded::byte_serialize(input.as_bytes()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration as StdDuration;
#[test]
fn test_client_creation() {
let config = Config::new("test_id", "test_key", "ap-beijing", "test-bucket-123")
.with_timeout(StdDuration::from_secs(60));
let client = CosClient::new(config);
assert!(client.is_ok());
}
#[test]
fn test_build_url() {
let config = Config::new("test_id", "test_key", "ap-beijing", "test-bucket-123");
let client = CosClient::new(config).unwrap();
let mut params = HashMap::new();
params.insert("key".to_string(), "value".to_string());
let url = client.build_url("/test", ¶ms).unwrap();
assert!(url.contains("test-bucket-123.cos.ap-beijing.myqcloud.com"));
assert!(url.contains("key=value"));
}
}