1use reqwest::header::HeaderValue;
2use serde_json::json;
3
4use crate::client::StorageClient;
5use crate::error::StorageError;
6use crate::types::*;
7
8#[derive(Debug, Clone)]
19pub struct StorageBucketApi {
20 client: StorageClient,
21 bucket_id: String,
22}
23
24impl StorageBucketApi {
25 pub(crate) fn new(client: StorageClient, bucket_id: String) -> Self {
26 Self { client, bucket_id }
27 }
28
29 pub async fn upload(
33 &self,
34 path: &str,
35 data: Vec<u8>,
36 options: FileOptions,
37 ) -> Result<UploadResponse, StorageError> {
38 let url = self
39 .client
40 .url(&format!("/object/{}/{}", self.bucket_id, path));
41
42 let content_type = options
43 .content_type
44 .as_deref()
45 .unwrap_or("application/octet-stream");
46
47 let mut req = self
48 .client
49 .http()
50 .post(url)
51 .header("content-type", content_type)
52 .body(data);
53
54 if let Some(cache) = &options.cache_control {
55 req = req.header("cache-control", cache.as_str());
56 }
57 if let Some(upsert) = options.upsert {
58 req = req.header("x-upsert", if upsert { "true" } else { "false" });
59 }
60 if let Some(metadata) = &options.metadata {
61 let meta_str = serde_json::to_string(metadata)?;
62 req = req.header("x-metadata", meta_str);
63 }
64
65 let resp = req.send().await?;
66 self.client.handle_response(resp).await
67 }
68
69 pub async fn update(
73 &self,
74 path: &str,
75 data: Vec<u8>,
76 options: Option<FileOptions>,
77 ) -> Result<UploadResponse, StorageError> {
78 let url = self
79 .client
80 .url(&format!("/object/{}/{}", self.bucket_id, path));
81
82 let opts = options.unwrap_or_default();
83 let content_type = opts
84 .content_type
85 .as_deref()
86 .unwrap_or("application/octet-stream");
87
88 let mut req = self
89 .client
90 .http()
91 .put(url)
92 .header("content-type", content_type)
93 .body(data);
94
95 if let Some(cache) = &opts.cache_control {
96 req = req.header("cache-control", cache.as_str());
97 }
98 if let Some(upsert) = opts.upsert {
99 req = req.header("x-upsert", if upsert { "true" } else { "false" });
100 }
101 if let Some(metadata) = &opts.metadata {
102 let meta_str = serde_json::to_string(metadata)?;
103 req = req.header("x-metadata", meta_str);
104 }
105
106 let resp = req.send().await?;
107 self.client.handle_response(resp).await
108 }
109
110 pub async fn download(&self, path: &str) -> Result<Vec<u8>, StorageError> {
114 let url = self
115 .client
116 .url(&format!("/object/{}/{}", self.bucket_id, path));
117 let resp = self.client.http().get(url).send().await?;
118 self.client.handle_bytes_response(resp).await
119 }
120
121 pub async fn list(
125 &self,
126 path: Option<&str>,
127 options: Option<SearchOptions>,
128 ) -> Result<Vec<FileObject>, StorageError> {
129 let url = self
130 .client
131 .url(&format!("/object/list/{}", self.bucket_id));
132
133 let mut body = json!({
134 "prefix": path.unwrap_or(""),
135 });
136
137 if let Some(opts) = options {
138 if let Some(limit) = opts.limit {
139 body["limit"] = json!(limit);
140 }
141 if let Some(offset) = opts.offset {
142 body["offset"] = json!(offset);
143 }
144 if let Some(sort_by) = opts.sort_by {
145 body["sortBy"] = json!(sort_by);
146 }
147 if let Some(search) = opts.search {
148 body["search"] = json!(search);
149 }
150 }
151
152 let resp = self.client.http().post(url).json(&body).send().await?;
153 self.client.handle_response(resp).await
154 }
155
156 pub async fn move_file(&self, from: &str, to: &str) -> Result<(), StorageError> {
160 let url = self.client.url("/object/move");
161 let body = json!({
162 "bucketId": self.bucket_id,
163 "sourceKey": from,
164 "destinationKey": to,
165 });
166
167 let resp = self.client.http().post(url).json(&body).send().await?;
168 self.client.handle_empty_response(resp).await
169 }
170
171 pub async fn copy(&self, from: &str, to: &str) -> Result<String, StorageError> {
175 let url = self.client.url("/object/copy");
176 let body = json!({
177 "bucketId": self.bucket_id,
178 "sourceKey": from,
179 "destinationKey": to,
180 });
181
182 let resp = self.client.http().post(url).json(&body).send().await?;
183 let result: serde_json::Value = self.client.handle_response(resp).await?;
184 Ok(result
185 .get("Key")
186 .or_else(|| result.get("key"))
187 .and_then(|v| v.as_str())
188 .unwrap_or(to)
189 .to_string())
190 }
191
192 pub async fn remove(&self, paths: Vec<&str>) -> Result<Vec<FileObject>, StorageError> {
196 let url = self
197 .client
198 .url(&format!("/object/{}", self.bucket_id));
199 let body = json!({
200 "prefixes": paths,
201 });
202
203 let resp = self.client.http().delete(url).json(&body).send().await?;
204 self.client.handle_response(resp).await
205 }
206
207 pub async fn create_signed_url(
211 &self,
212 path: &str,
213 expires_in: u64,
214 ) -> Result<SignedUrlResponse, StorageError> {
215 let url = self
216 .client
217 .url(&format!("/object/sign/{}/{}", self.bucket_id, path));
218 let body = json!({ "expiresIn": expires_in });
219
220 let resp = self.client.http().post(url).json(&body).send().await?;
221 let mut result: SignedUrlResponse = self.client.handle_response(resp).await?;
222
223 if result.signed_url.starts_with('/') {
225 let base = self.client.base_url().as_str().trim_end_matches('/');
226 result.signed_url = format!("{}{}", base, result.signed_url);
227 }
228
229 Ok(result)
230 }
231
232 pub async fn create_signed_urls(
236 &self,
237 paths: Vec<&str>,
238 expires_in: u64,
239 ) -> Result<Vec<SignedUrlBatchEntry>, StorageError> {
240 let url = self
241 .client
242 .url(&format!("/object/sign/{}", self.bucket_id));
243 let body = json!({
244 "expiresIn": expires_in,
245 "paths": paths,
246 });
247
248 let resp = self.client.http().post(url).json(&body).send().await?;
249 let mut results: Vec<SignedUrlBatchEntry> = self.client.handle_response(resp).await?;
250
251 let base = self.client.base_url().as_str().trim_end_matches('/');
253 for entry in &mut results {
254 if let Some(ref mut signed_url) = entry.signed_url {
255 if signed_url.starts_with('/') {
256 *signed_url = format!("{}{}", base, signed_url);
257 }
258 }
259 }
260
261 Ok(results)
262 }
263
264 pub fn get_public_url(&self, path: &str) -> String {
268 let base = self.client.base_url().as_str().trim_end_matches('/');
269 format!("{}/object/public/{}/{}", base, self.bucket_id, path)
270 }
271
272 pub async fn create_signed_upload_url(
274 &self,
275 path: &str,
276 ) -> Result<SignedUploadUrlResponse, StorageError> {
277 let url = self.client.url(&format!(
278 "/object/upload/sign/{}/{}",
279 self.bucket_id, path
280 ));
281
282 let resp = self
283 .client
284 .http()
285 .post(url)
286 .json(&json!({}))
287 .send()
288 .await?;
289 self.client.handle_response(resp).await
290 }
291
292 pub async fn upload_to_signed_url(
294 &self,
295 token: &str,
296 path: &str,
297 data: Vec<u8>,
298 options: Option<FileOptions>,
299 ) -> Result<(), StorageError> {
300 let url = self.client.url(&format!(
301 "/object/upload/sign/{}/{}?token={}",
302 self.bucket_id, path, token
303 ));
304
305 let opts = options.unwrap_or_default();
306 let content_type = opts
307 .content_type
308 .as_deref()
309 .unwrap_or("application/octet-stream");
310
311 let mut req = self
312 .client
313 .http()
314 .put(url)
315 .header("content-type", content_type)
316 .body(data);
317
318 if let Some(cache) = &opts.cache_control {
319 req = req.header(
320 "cache-control",
321 HeaderValue::from_str(cache)
322 .map_err(|e| StorageError::InvalidConfig(format!("Invalid header: {}", e)))?,
323 );
324 }
325
326 let resp = req.send().await?;
327 self.client.handle_empty_response(resp).await
328 }
329
330 pub async fn info(&self, path: &str) -> Result<FileInfo, StorageError> {
334 let url = self.client.url(&format!(
335 "/object/info/authenticated/{}/{}",
336 self.bucket_id, path
337 ));
338 let resp = self.client.http().get(url).send().await?;
339 self.client.handle_response(resp).await
340 }
341
342 pub async fn exists(&self, path: &str) -> Result<bool, StorageError> {
347 let url = self
348 .client
349 .url(&format!("/object/{}/{}", self.bucket_id, path));
350 let resp = self.client.http().head(url).send().await?;
351 let status = resp.status().as_u16();
352 if status >= 200 && status < 300 {
353 Ok(true)
354 } else if status == 404 || status == 400 {
355 Ok(false)
356 } else {
357 Err(StorageError::Api {
358 status,
359 message: format!("HTTP {}", status),
360 })
361 }
362 }
363
364 pub async fn download_with_transform(
368 &self,
369 path: &str,
370 transform: &TransformOptions,
371 ) -> Result<Vec<u8>, StorageError> {
372 let qs = transform.to_query_string();
373 let url_path = if qs.is_empty() {
374 format!(
375 "/render/image/authenticated/{}/{}",
376 self.bucket_id, path
377 )
378 } else {
379 format!(
380 "/render/image/authenticated/{}/{}?{}",
381 self.bucket_id, path, qs
382 )
383 };
384 let url = self.client.url(&url_path);
385 let resp = self.client.http().get(url).send().await?;
386 self.client.handle_bytes_response(resp).await
387 }
388
389 pub fn get_public_url_with_transform(
393 &self,
394 path: &str,
395 transform: &TransformOptions,
396 ) -> String {
397 let base = self.client.base_url().as_str().trim_end_matches('/');
398 let qs = transform.to_query_string();
399 if qs.is_empty() {
400 format!(
401 "{}/render/image/public/{}/{}",
402 base, self.bucket_id, path
403 )
404 } else {
405 format!(
406 "{}/render/image/public/{}/{}?{}",
407 base, self.bucket_id, path, qs
408 )
409 }
410 }
411
412 pub async fn create_signed_url_with_transform(
416 &self,
417 path: &str,
418 expires_in: u64,
419 transform: &TransformOptions,
420 ) -> Result<SignedUrlResponse, StorageError> {
421 let url = self
422 .client
423 .url(&format!("/object/sign/{}/{}", self.bucket_id, path));
424 let mut body = json!({ "expiresIn": expires_in });
425 if !transform.is_empty() {
426 body["transform"] = transform.to_json();
427 }
428
429 let resp = self.client.http().post(url).json(&body).send().await?;
430 let mut result: SignedUrlResponse = self.client.handle_response(resp).await?;
431
432 if result.signed_url.starts_with('/') {
433 let base = self.client.base_url().as_str().trim_end_matches('/');
434 result.signed_url = format!("{}{}", base, result.signed_url);
435 }
436
437 Ok(result)
438 }
439
440 pub async fn create_signed_urls_with_transform(
444 &self,
445 paths: Vec<&str>,
446 expires_in: u64,
447 transform: &TransformOptions,
448 ) -> Result<Vec<SignedUrlBatchEntry>, StorageError> {
449 let url = self
450 .client
451 .url(&format!("/object/sign/{}", self.bucket_id));
452 let mut body = json!({
453 "expiresIn": expires_in,
454 "paths": paths,
455 });
456 if !transform.is_empty() {
457 body["transform"] = transform.to_json();
458 }
459
460 let resp = self.client.http().post(url).json(&body).send().await?;
461 let mut results: Vec<SignedUrlBatchEntry> = self.client.handle_response(resp).await?;
462
463 let base = self.client.base_url().as_str().trim_end_matches('/');
464 for entry in &mut results {
465 if let Some(ref mut signed_url) = entry.signed_url {
466 if signed_url.starts_with('/') {
467 *signed_url = format!("{}{}", base, signed_url);
468 }
469 }
470 }
471
472 Ok(results)
473 }
474
475 pub async fn move_to_bucket(
479 &self,
480 from: &str,
481 to_bucket: &str,
482 to_path: &str,
483 ) -> Result<(), StorageError> {
484 let url = self.client.url("/object/move");
485 let body = json!({
486 "bucketId": self.bucket_id,
487 "sourceKey": from,
488 "destinationBucket": to_bucket,
489 "destinationKey": to_path,
490 });
491
492 let resp = self.client.http().post(url).json(&body).send().await?;
493 self.client.handle_empty_response(resp).await
494 }
495
496 pub async fn copy_to_bucket(
502 &self,
503 from: &str,
504 to_bucket: &str,
505 to_path: &str,
506 ) -> Result<String, StorageError> {
507 let url = self.client.url("/object/copy");
508 let body = json!({
509 "bucketId": self.bucket_id,
510 "sourceKey": from,
511 "destinationBucket": to_bucket,
512 "destinationKey": to_path,
513 });
514
515 let resp = self.client.http().post(url).json(&body).send().await?;
516 let result: serde_json::Value = self.client.handle_response(resp).await?;
517 Ok(result
518 .get("Key")
519 .or_else(|| result.get("key"))
520 .and_then(|v| v.as_str())
521 .unwrap_or(to_path)
522 .to_string())
523 }
524
525 pub fn get_public_url_with_download(&self, path: &str, filename: Option<&str>) -> String {
532 let base_url = self.get_public_url(path);
533 match filename {
534 Some(name) => format!("{}?download={}", base_url, name),
535 None => base_url,
536 }
537 }
538
539 pub async fn create_signed_url_with_download(
546 &self,
547 path: &str,
548 expires_in: u64,
549 filename: Option<&str>,
550 ) -> Result<SignedUrlResponse, StorageError> {
551 let mut result = self.create_signed_url(path, expires_in).await?;
552 if let Some(name) = filename {
553 let separator = if result.signed_url.contains('?') { "&" } else { "?" };
554 result.signed_url = format!("{}{}download={}", result.signed_url, separator, name);
555 }
556 Ok(result)
557 }
558
559 pub fn bucket_id(&self) -> &str {
561 &self.bucket_id
562 }
563}