1use crate::client::CosClient;
6use crate::error::{CosError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10use tokio::fs::File;
11use tokio::io::{AsyncReadExt, AsyncWriteExt};
12
13#[derive(Debug, Clone)]
15pub struct ObjectClient {
16 client: CosClient,
17}
18
19impl ObjectClient {
20 pub fn new(client: CosClient) -> Self {
22 Self { client }
23 }
24
25 pub async fn put_object(
27 &self,
28 key: &str,
29 data: Vec<u8>,
30 content_type: Option<&str>,
31 ) -> Result<PutObjectResponse> {
32 let params = HashMap::new();
33
34 let mut headers = HashMap::new();
35 if let Some(ct) = content_type {
36 headers.insert("Content-Type".to_string(), ct.to_string());
37 }
38 headers.insert("Content-Length".to_string(), data.len().to_string());
39
40 let response = self.client.put(&format!("/{}", key), params, Some(data)).await?;
41
42 Ok(PutObjectResponse {
43 etag: response
44 .headers()
45 .get("etag")
46 .and_then(|v| v.to_str().ok())
47 .unwrap_or("")
48 .to_string(),
49 version_id: response
50 .headers()
51 .get("x-cos-version-id")
52 .and_then(|v| v.to_str().ok())
53 .map(|s| s.to_string()),
54 })
55 }
56
57 pub async fn put_object_from_file(
59 &self,
60 key: &str,
61 file_path: &Path,
62 content_type: Option<&str>,
63 ) -> Result<PutObjectResponse> {
64 let mut file = File::open(file_path)
65 .await
66 .map_err(|e| CosError::other(format!("Failed to open file: {}", e)))?;
67
68 let mut data = Vec::new();
69 file.read_to_end(&mut data)
70 .await
71 .map_err(|e| CosError::other(format!("Failed to read file: {}", e)))?;
72
73 let content_type = content_type.or_else(|| {
74 file_path
75 .extension()
76 .and_then(|ext| ext.to_str())
77 .and_then(|ext| match ext.to_lowercase().as_str() {
78 "txt" => Some("text/plain"),
80 "html" | "htm" => Some("text/html"),
81 "css" => Some("text/css"),
82 "js" => Some("application/javascript"),
83 "json" => Some("application/json"),
84 "xml" => Some("application/xml"),
85 "csv" => Some("text/csv"),
86 "md" => Some("text/markdown"),
87
88 "jpg" | "jpeg" => Some("image/jpeg"),
90 "png" => Some("image/png"),
91 "gif" => Some("image/gif"),
92 "webp" => Some("image/webp"),
93 "bmp" => Some("image/bmp"),
94 "tiff" | "tif" => Some("image/tiff"),
95 "svg" => Some("image/svg+xml"),
96 "ico" => Some("image/x-icon"),
97 "heic" => Some("image/heic"),
98 "heif" => Some("image/heif"),
99 "avif" => Some("image/avif"),
100 "jxl" => Some("image/jxl"),
101
102 "mp4" => Some("video/mp4"),
104 "avi" => Some("video/x-msvideo"),
105 "mov" => Some("video/quicktime"),
106 "wmv" => Some("video/x-ms-wmv"),
107 "flv" => Some("video/x-flv"),
108 "webm" => Some("video/webm"),
109 "mkv" => Some("video/x-matroska"),
110 "m4v" => Some("video/x-m4v"),
111 "3gp" => Some("video/3gpp"),
112 "3g2" => Some("video/3gpp2"),
113 "ts" => Some("video/mp2t"),
114 "mts" => Some("video/mp2t"),
115 "m2ts" => Some("video/mp2t"),
116 "ogv" => Some("video/ogg"),
117
118 "mp3" => Some("audio/mpeg"),
120 "wav" => Some("audio/wav"),
121 "flac" => Some("audio/flac"),
122 "aac" => Some("audio/aac"),
123 "ogg" => Some("audio/ogg"),
124 "wma" => Some("audio/x-ms-wma"),
125 "m4a" => Some("audio/mp4"),
126 "opus" => Some("audio/opus"),
127
128 "pdf" => Some("application/pdf"),
130 "doc" => Some("application/msword"),
131 "docx" => Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
132 "xls" => Some("application/vnd.ms-excel"),
133 "xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
134 "ppt" => Some("application/vnd.ms-powerpoint"),
135 "pptx" => Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
136 "rtf" => Some("application/rtf"),
137
138 "zip" => Some("application/zip"),
140 "rar" => Some("application/vnd.rar"),
141 "7z" => Some("application/x-7z-compressed"),
142 "tar" => Some("application/x-tar"),
143 "gz" => Some("application/gzip"),
144 "bz2" => Some("application/x-bzip2"),
145
146 "bin" => Some("application/octet-stream"),
148 "exe" => Some("application/octet-stream"),
149 "dmg" => Some("application/x-apple-diskimage"),
150 "iso" => Some("application/x-iso9660-image"),
151
152 _ => None,
153 })
154 });
155
156 self.put_object(key, data, content_type).await
157 }
158
159 pub async fn get_object(&self, key: &str) -> Result<GetObjectResponse> {
161 let params = HashMap::new();
162 let response = self.client.get(&format!("/{}", key), params).await?;
163
164 let content_length = response
165 .headers()
166 .get("content-length")
167 .and_then(|v| v.to_str().ok())
168 .and_then(|s| s.parse().ok())
169 .unwrap_or(0);
170
171 let content_type = response
172 .headers()
173 .get("content-type")
174 .and_then(|v| v.to_str().ok())
175 .unwrap_or("application/octet-stream")
176 .to_string();
177
178 let etag = response
179 .headers()
180 .get("etag")
181 .and_then(|v| v.to_str().ok())
182 .unwrap_or("")
183 .to_string();
184
185 let last_modified = response
186 .headers()
187 .get("last-modified")
188 .and_then(|v| v.to_str().ok())
189 .map(|s| s.to_string());
190
191 let data = response
192 .bytes()
193 .await
194 .map_err(|e| CosError::other(format!("Failed to read response body: {}", e)))?
195 .to_vec();
196
197 Ok(GetObjectResponse {
198 data,
199 content_length,
200 content_type,
201 etag,
202 last_modified,
203 })
204 }
205
206 pub async fn get_object_to_file(&self, key: &str, file_path: &Path) -> Result<()> {
208 let response = self.get_object(key).await?;
209
210 let mut file = File::create(file_path)
211 .await
212 .map_err(|e| CosError::other(format!("Failed to create file: {}", e)))?;
213
214 file.write_all(&response.data)
215 .await
216 .map_err(|e| CosError::other(format!("Failed to write file: {}", e)))?;
217
218 Ok(())
219 }
220
221 pub async fn delete_object(&self, key: &str) -> Result<DeleteObjectResponse> {
223 let params = HashMap::new();
224 let response = self.client.delete(&format!("/{}", key), params).await?;
225
226 Ok(DeleteObjectResponse {
227 version_id: response
228 .headers()
229 .get("x-cos-version-id")
230 .and_then(|v| v.to_str().ok())
231 .map(|s| s.to_string()),
232 delete_marker: response
233 .headers()
234 .get("x-cos-delete-marker")
235 .and_then(|v| v.to_str().ok())
236 .and_then(|s| s.parse().ok())
237 .unwrap_or(false),
238 })
239 }
240
241 pub async fn delete_objects(&self, keys: &[String]) -> Result<DeleteObjectsResponse> {
243 let delete_request = DeleteRequest {
244 objects: keys.iter().map(|key| DeleteObject {
245 key: key.clone(),
246 version_id: None,
247 }).collect(),
248 quiet: false,
249 };
250
251 let xml_body = quick_xml::se::to_string(&delete_request)
252 .map_err(|e| CosError::other(format!("Failed to serialize delete request: {}", e)))?;
253
254 let mut params = HashMap::new();
255 params.insert("delete".to_string(), "".to_string());
256
257 let response = self.client.post("/", params, Some(xml_body)).await?;
258
259 let response_text = response
260 .text()
261 .await
262 .map_err(|e| CosError::other(format!("Failed to read response: {}", e)))?;
263
264 let delete_response: DeleteObjectsResponse = quick_xml::de::from_str(&response_text)
265 .map_err(|e| CosError::other(format!("Failed to parse delete response: {}", e)))?;
266
267 Ok(delete_response)
268 }
269
270 pub async fn head_object(&self, key: &str) -> Result<HeadObjectResponse> {
272 let params = HashMap::new();
273 let response = self.client.head(&format!("/{}", key), params).await?;
274
275 let content_length = response
276 .headers()
277 .get("content-length")
278 .and_then(|v| v.to_str().ok())
279 .and_then(|s| s.parse().ok())
280 .unwrap_or(0);
281
282 let content_type = response
283 .headers()
284 .get("content-type")
285 .and_then(|v| v.to_str().ok())
286 .unwrap_or("application/octet-stream")
287 .to_string();
288
289 let etag = response
290 .headers()
291 .get("etag")
292 .and_then(|v| v.to_str().ok())
293 .unwrap_or("")
294 .to_string();
295
296 let last_modified = response
297 .headers()
298 .get("last-modified")
299 .and_then(|v| v.to_str().ok())
300 .map(|s| s.to_string());
301
302 Ok(HeadObjectResponse {
303 content_length,
304 content_type,
305 etag,
306 last_modified,
307 })
308 }
309
310 pub async fn object_exists(&self, key: &str) -> Result<bool> {
312 match self.head_object(key).await {
313 Ok(_) => Ok(true),
314 Err(CosError::Server { .. }) => Ok(false),
315 Err(e) => Err(e),
316 }
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct PutObjectResponse {
323 pub etag: String,
324 pub version_id: Option<String>,
325}
326
327#[derive(Debug, Clone)]
329pub struct GetObjectResponse {
330 pub data: Vec<u8>,
331 pub content_length: u64,
332 pub content_type: String,
333 pub etag: String,
334 pub last_modified: Option<String>,
335}
336
337#[derive(Debug, Clone)]
339pub struct DeleteObjectResponse {
340 pub version_id: Option<String>,
341 pub delete_marker: bool,
342}
343
344#[derive(Debug, Clone)]
346pub struct HeadObjectResponse {
347 pub content_length: u64,
348 pub content_type: String,
349 pub etag: String,
350 pub last_modified: Option<String>,
351}
352
353#[derive(Debug, Serialize)]
355#[serde(rename = "Delete")]
356struct DeleteRequest {
357 #[serde(rename = "Object")]
358 objects: Vec<DeleteObject>,
359 #[serde(rename = "Quiet")]
360 quiet: bool,
361}
362
363#[derive(Debug, Serialize)]
365struct DeleteObject {
366 #[serde(rename = "Key")]
367 key: String,
368 #[serde(rename = "VersionId", skip_serializing_if = "Option::is_none")]
369 version_id: Option<String>,
370}
371
372#[derive(Debug, Deserialize)]
374#[serde(rename = "DeleteResult")]
375pub struct DeleteObjectsResponse {
376 #[serde(rename = "Deleted", default)]
377 pub deleted: Vec<DeletedObject>,
378 #[serde(rename = "Error", default)]
379 pub errors: Vec<DeleteError>,
380}
381
382#[derive(Debug, Deserialize)]
384pub struct DeletedObject {
385 #[serde(rename = "Key")]
386 pub key: String,
387 #[serde(rename = "VersionId")]
388 pub version_id: Option<String>,
389 #[serde(rename = "DeleteMarker")]
390 pub delete_marker: Option<bool>,
391}
392
393#[derive(Debug, Deserialize)]
395pub struct DeleteError {
396 #[serde(rename = "Key")]
397 pub key: String,
398 #[serde(rename = "Code")]
399 pub code: String,
400 #[serde(rename = "Message")]
401 pub message: String,
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::config::Config;
408 use std::time::Duration;
409
410 #[tokio::test]
411 async fn test_object_operations() {
412 let config = Config::new("test_id", "test_key", "ap-beijing", "test-bucket-123")
413 .with_timeout(Duration::from_secs(60));
414
415 let cos_client = CosClient::new(config).unwrap();
416 let object_client = ObjectClient::new(cos_client);
417
418 let exists = object_client.object_exists("test-key").await;
420 }
422}