1use reqwest::Client;
7use reqwest::multipart::{Form, Part};
8use serde::{Serialize, Deserialize};
9use thiserror::Error;
10use std::collections::HashMap;
11use url::Url;
12use bytes::Bytes;
13use std::path::Path;
14use tokio::fs::File;
15use tokio::io::AsyncReadExt;
16
17#[cfg(test)]
19use wiremock::{MockServer, Mock, ResponseTemplate};
20#[cfg(test)]
21use wiremock::matchers::{method, path, query_param};
22#[cfg(test)]
23use serde_json::json;
24
25pub type Result<T> = std::result::Result<T, StorageError>;
27
28#[derive(Error, Debug)]
30pub enum StorageError {
31 #[error("API error: {0}")]
32 ApiError(String),
33
34 #[error("Network error: {0}")]
35 NetworkError(#[from] reqwest::Error),
36
37 #[error("JSON serialization error: {0}")]
38 SerializationError(#[from] serde_json::Error),
39
40 #[error("URL parse error: {0}")]
41 UrlParseError(#[from] url::ParseError),
42
43 #[error("Storage error: {0}")]
44 StorageError(String),
45
46 #[error("File not found: {0}")]
47 FileNotFound(String),
48
49 #[error("IO error: {0}")]
50 IoError(#[from] std::io::Error),
51
52 #[error("Request error: {0}")]
53 RequestError(String),
54
55 #[error("Deserialization error: {0}")]
56 DeserializationError(String),
57}
58
59impl StorageError {
60 pub fn new(message: String) -> Self {
61 Self::StorageError(message)
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Default)]
67pub struct FileOptions {
68 pub cache_control: Option<String>,
69 pub content_type: Option<String>,
70 pub upsert: Option<bool>,
71}
72
73impl FileOptions {
74 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn with_cache_control(mut self, cache_control: &str) -> Self {
81 self.cache_control = Some(cache_control.to_string());
82 self
83 }
84
85 pub fn with_content_type(mut self, content_type: &str) -> Self {
87 self.content_type = Some(content_type.to_string());
88 self
89 }
90
91 pub fn with_upsert(mut self, upsert: bool) -> Self {
93 self.upsert = Some(upsert);
94 self
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Default)]
100pub struct ListOptions {
101 pub limit: Option<i32>,
102 pub offset: Option<i32>,
103 pub sort_by: Option<SortBy>,
104 pub search: Option<String>,
105}
106
107impl ListOptions {
108 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn limit(mut self, limit: i32) -> Self {
115 self.limit = Some(limit);
116 self
117 }
118
119 pub fn offset(mut self, offset: i32) -> Self {
121 self.offset = Some(offset);
122 self
123 }
124
125 pub fn sort_by(mut self, column: &str, order: SortOrder) -> Self {
127 self.sort_by = Some(SortBy {
128 column: column.to_string(),
129 order,
130 });
131 self
132 }
133
134 pub fn search(mut self, search: &str) -> Self {
136 self.search = Some(search.to_string());
137 self
138 }
139}
140
141#[derive(Debug, Clone, Serialize)]
143pub struct SortBy {
144 pub column: String,
145 pub order: SortOrder,
146}
147
148impl ToString for SortBy {
149 fn to_string(&self) -> String {
150 format!("{}:{:?}", self.column, self.order).to_lowercase()
151 }
152}
153
154#[derive(Debug, Clone, Copy, Serialize)]
156#[serde(rename_all = "lowercase")]
157pub enum SortOrder {
158 Asc,
159 Desc,
160}
161
162#[derive(Debug, Clone, Serialize, Default)]
164pub struct ImageTransformOptions {
165 pub width: Option<u32>,
166 pub height: Option<u32>,
167 pub resize: Option<String>,
168 pub format: Option<String>,
169 pub quality: Option<u32>,
170}
171
172impl ImageTransformOptions {
173 pub fn new() -> Self {
175 Self::default()
176 }
177
178 pub fn with_width(mut self, width: u32) -> Self {
180 self.width = Some(width);
181 self
182 }
183
184 pub fn with_height(mut self, height: u32) -> Self {
186 self.height = Some(height);
187 self
188 }
189
190 pub fn with_resize(mut self, resize: &str) -> Self {
192 self.resize = Some(resize.to_string());
193 self
194 }
195
196 pub fn with_format(mut self, format: &str) -> Self {
198 self.format = Some(format.to_string());
199 self
200 }
201
202 pub fn with_quality(mut self, quality: u32) -> Self {
204 self.quality = Some(quality.min(100));
205 self
206 }
207
208 fn to_query_params(&self) -> String {
210 let mut params = Vec::new();
211
212 if let Some(width) = self.width {
213 params.push(format!("width={}", width));
214 }
215
216 if let Some(height) = self.height {
217 params.push(format!("height={}", height));
218 }
219
220 if let Some(resize) = &self.resize {
221 params.push(format!("resize={}", resize));
222 }
223
224 if let Some(format) = &self.format {
225 params.push(format!("format={}", format));
226 }
227
228 if let Some(quality) = self.quality {
229 params.push(format!("quality={}", quality));
230 }
231
232 if params.is_empty() {
233 String::new()
234 } else {
235 format!("?{}", params.join("&"))
236 }
237 }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct FileObject {
243 pub name: String,
244 pub bucket_id: String,
245 pub owner: String,
246 pub id: String,
247 pub updated_at: String,
248 pub created_at: String,
249 pub last_accessed_at: String,
250 pub metadata: Option<serde_json::Value>,
251 pub mime_type: Option<String>,
252 pub size: i64,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct Bucket {
258 pub id: String,
259 pub name: String,
260 pub owner: String,
261 pub public: bool,
262 pub created_at: String,
263 pub updated_at: String,
264}
265
266#[derive(Debug, Clone, Deserialize)]
268pub struct InitiateMultipartUploadResponse {
269 pub id: String,
270 #[serde(rename = "uploadId")]
271 pub upload_id: String,
272 pub key: String,
273 pub bucket: String,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct UploadedPartInfo {
279 #[serde(rename = "partNumber")]
280 pub part_number: u32,
281 pub etag: String,
282}
283
284#[derive(Debug, Clone, Serialize)]
286struct CompleteMultipartUploadRequest {
287 #[serde(rename = "uploadId")]
288 pub upload_id: String,
289 pub parts: Vec<UploadedPartInfo>,
290}
291
292pub struct StorageBucketClient<'a> {
294 parent: &'a StorageClient,
295 bucket_id: String,
296}
297
298pub struct StorageClient {
300 base_url: String,
301 api_key: String,
302 http_client: Client,
303}
304
305impl StorageClient {
306 pub fn new(base_url: &str, api_key: &str, http_client: Client) -> Self {
308 Self {
309 base_url: base_url.to_string(),
310 api_key: api_key.to_string(),
311 http_client,
312 }
313 }
314
315 pub fn from<'a>(&'a self, bucket_id: &str) -> StorageBucketClient<'a> {
317 StorageBucketClient {
318 parent: self,
319 bucket_id: bucket_id.to_string(),
320 }
321 }
322
323 pub async fn list_buckets(&self) -> Result<Vec<Bucket>> {
325 let url = format!("{}/storage/v1/bucket", self.base_url);
326
327 let response = self.http_client.get(&url)
328 .header("apikey", &self.api_key)
329 .send()
330 .await?;
331
332 if !response.status().is_success() {
333 let error_text = response.text().await?;
334 return Err(StorageError::ApiError(error_text));
335 }
336
337 let buckets = response.json::<Vec<Bucket>>().await?;
338
339 Ok(buckets)
340 }
341
342 pub async fn create_bucket(&self, bucket_id: &str, is_public: bool) -> Result<Bucket> {
344 let url = format!("{}/storage/v1/bucket", self.base_url);
345
346 let payload = serde_json::json!({
347 "id": bucket_id,
348 "name": bucket_id,
349 "public": is_public
350 });
351
352 let response = self.http_client.post(&url)
353 .header("apikey", &self.api_key)
354 .header("Content-Type", "application/json")
355 .json(&payload)
356 .send()
357 .await?;
358
359 if !response.status().is_success() {
360 let error_text = response.text().await?;
361 return Err(StorageError::ApiError(error_text));
362 }
363
364 let bucket = response.json::<Bucket>().await?;
365
366 Ok(bucket)
367 }
368
369 pub async fn delete_bucket(&self, bucket_id: &str) -> Result<()> {
371 let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_id);
372
373 let response = self.http_client.delete(&url)
374 .header("apikey", &self.api_key)
375 .send()
376 .await?;
377
378 if !response.status().is_success() {
379 let error_text = response.text().await?;
380 return Err(StorageError::ApiError(error_text));
381 }
382
383 Ok(())
384 }
385
386 pub async fn update_bucket(&self, bucket_id: &str, is_public: bool) -> Result<Bucket> {
388 let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_id);
389
390 let payload = serde_json::json!({
391 "id": bucket_id,
392 "public": is_public
393 });
394
395 let response = self.http_client.put(&url)
396 .header("apikey", &self.api_key)
397 .header("Content-Type", "application/json")
398 .json(&payload)
399 .send()
400 .await?;
401
402 if !response.status().is_success() {
403 let error_text = response.text().await?;
404 return Err(StorageError::ApiError(error_text));
405 }
406
407 let bucket = response.json::<Bucket>().await?;
408
409 Ok(bucket)
410 }
411}
412
413impl<'a> StorageBucketClient<'a> {
414 pub async fn upload(&self, path: &str, file_path: &Path, options: Option<FileOptions>) -> Result<FileObject> {
416 let mut url = Url::parse(&self.parent.base_url)?;
417 url.set_path(&format!("/storage/v1/object/{}/{}", self.bucket_id, path));
418
419 if let Some(opts) = &options {
421 let mut query_pairs = url.query_pairs_mut();
422 if let Some(cache_control) = &opts.cache_control {
423 query_pairs.append_pair("cache_control", cache_control);
424 }
425 if let Some(upsert) = &opts.upsert {
426 query_pairs.append_pair("upsert", &upsert.to_string());
427 }
428 }
429
430 let mut file = File::open(file_path).await?;
432 let mut contents = Vec::new();
433 file.read_to_end(&mut contents).await?;
434
435 let part = Part::bytes(contents)
437 .file_name(file_path.file_name().unwrap().to_string_lossy().to_string());
438
439 let form = Form::new().part("file", part);
440
441 let response = self.parent.http_client
442 .post(url)
443 .header("apikey", &self.parent.api_key)
444 .header("Authorization", format!("Bearer {}", &self.parent.api_key))
445 .multipart(form)
446 .send()
447 .await?;
448
449 if !response.status().is_success() {
450 let error_text = response.text().await?;
451 return Err(StorageError::ApiError(error_text));
452 }
453
454 let file_object = response.json::<FileObject>().await?;
455
456 Ok(file_object)
457 }
458
459 pub async fn download(&self, path: &str) -> Result<Bytes> {
461 let mut url = Url::parse(&self.parent.base_url)?;
462 url.set_path(&format!("/storage/v1/object/{}/{}", self.bucket_id, path));
463
464 let response = self.parent.http_client
465 .get(url)
466 .header("apikey", &self.parent.api_key)
467 .header("Authorization", format!("Bearer {}", &self.parent.api_key))
468 .send()
469 .await?;
470
471 if !response.status().is_success() {
472 let error_text = response.text().await?;
473 return Err(StorageError::ApiError(error_text));
474 }
475
476 let bytes = response.bytes().await?;
477
478 Ok(bytes)
479 }
480
481 pub async fn list(&self, prefix: &str, options: Option<ListOptions>) -> Result<Vec<FileObject>> {
483 let mut url = Url::parse(&self.parent.base_url)?;
484 url.set_path(&format!("/storage/v1/object/list/{}", self.bucket_id));
485
486 {
488 let mut query_pairs = url.query_pairs_mut();
489 query_pairs.append_pair("prefix", prefix);
490
491 if let Some(opts) = &options {
492 if let Some(limit) = opts.limit {
493 query_pairs.append_pair("limit", &limit.to_string());
494 }
495 if let Some(offset) = opts.offset {
496 query_pairs.append_pair("offset", &offset.to_string());
497 }
498 if let Some(sort_by) = &opts.sort_by {
499 query_pairs.append_pair("sortBy", &sort_by.to_string());
500 }
501 if let Some(search) = &opts.search {
502 query_pairs.append_pair("search", search);
503 }
504 }
505 } let response = self.parent.http_client
508 .get(url)
509 .header("apikey", &self.parent.api_key)
510 .header("Authorization", format!("Bearer {}", &self.parent.api_key))
511 .send()
512 .await?;
513
514 if !response.status().is_success() {
515 let error_text = response.text().await?;
516 return Err(StorageError::ApiError(error_text));
517 }
518
519 let files = response.json::<Vec<FileObject>>().await?;
520
521 Ok(files)
522 }
523
524 pub async fn remove(&self, paths: Vec<&str>) -> Result<()> {
526 let url = format!("{}/storage/v1/object/{}", self.parent.base_url, self.bucket_id);
527
528 let payload = serde_json::json!({
529 "prefixes": paths
530 });
531
532 let response = self.parent.http_client.delete(&url)
533 .header("apikey", &self.parent.api_key)
534 .header("Content-Type", "application/json")
535 .json(&payload)
536 .send()
537 .await?;
538
539 if !response.status().is_success() {
540 let error_text = response.text().await?;
541 return Err(StorageError::ApiError(error_text));
542 }
543
544 Ok(())
545 }
546
547 pub fn get_public_url(&self, path: &str) -> String {
549 format!("{}/storage/v1/object/public/{}/{}", self.parent.base_url, self.bucket_id, path)
550 }
551
552 pub async fn create_signed_url(&self, path: &str, expires_in: i32) -> Result<String> {
554 let url = format!("{}/storage/v1/object/sign/{}/{}", self.parent.base_url, self.bucket_id, path);
555
556 let payload = serde_json::json!({
557 "expiresIn": expires_in
558 });
559
560 let response = self.parent.http_client.post(&url)
561 .header("apikey", &self.parent.api_key)
562 .header("Content-Type", "application/json")
563 .json(&payload)
564 .send()
565 .await?;
566
567 if !response.status().is_success() {
568 let error_text = response.text().await?;
569 return Err(StorageError::ApiError(error_text));
570 }
571
572 #[derive(Deserialize)]
573 struct SignedUrlResponse {
574 signed_url: String,
575 }
576
577 let signed_url = response.json::<SignedUrlResponse>().await?;
578
579 Ok(signed_url.signed_url)
580 }
581
582 pub async fn initiate_multipart_upload(
584 &self,
585 path: &str,
586 options: Option<FileOptions>
587 ) -> Result<InitiateMultipartUploadResponse> {
588 let url = format!(
589 "{}/storage/v1/upload/initiate",
590 self.parent.base_url
591 );
592
593 let options = options.unwrap_or_default();
594
595 let cache_control = options.cache_control.unwrap_or_else(|| "max-age=3600".to_string());
596 let content_type = options.content_type.unwrap_or_else(|| "application/octet-stream".to_string());
597 let upsert = options.upsert.unwrap_or(false);
598
599 let payload = serde_json::json!({
600 "bucket": self.bucket_id,
601 "name": path,
602 "cacheControl": cache_control,
603 "contentType": content_type,
604 "upsert": upsert,
605 });
606
607 let response = self.parent.http_client.post(&url)
608 .header("apikey", &self.parent.api_key)
609 .header("Content-Type", "application/json")
610 .json(&payload)
611 .send()
612 .await?;
613
614 if !response.status().is_success() {
615 let error_text = response.text().await?;
616 return Err(StorageError::ApiError(error_text));
617 }
618
619 let initiate_response: InitiateMultipartUploadResponse = response.json().await?;
620
621 Ok(initiate_response)
622 }
623
624 pub async fn upload_part(
626 &self,
627 upload_id: &str,
628 part_number: u32,
629 data: Bytes
630 ) -> Result<UploadedPartInfo> {
631 let url = format!(
632 "{}/storage/v1/upload/part",
633 self.parent.base_url
634 );
635
636 let body = reqwest::Body::from(data);
637
638 let response = self.parent.http_client.post(&url)
639 .header("apikey", &self.parent.api_key)
640 .query(&[
641 ("uploadId", upload_id),
642 ("partNumber", &part_number.to_string()),
643 ("bucket", &self.bucket_id),
644 ])
645 .body(body)
646 .send()
647 .await?;
648
649 if !response.status().is_success() {
650 let error_text = response.text().await?;
651 return Err(StorageError::ApiError(error_text));
652 }
653
654 let etag = response.headers()
655 .get("etag")
656 .ok_or_else(|| StorageError::new("ETag header not found in response".to_string()))?
657 .to_str()
658 .map_err(|e| StorageError::new(format!("Invalid ETag header: {}", e)))?
659 .to_string();
660
661 let part_info = UploadedPartInfo {
662 part_number,
663 etag,
664 };
665
666 Ok(part_info)
667 }
668
669 pub async fn complete_multipart_upload(
671 &self,
672 upload_id: &str,
673 path: &str,
674 parts: Vec<UploadedPartInfo>
675 ) -> Result<FileObject> {
676 let url = format!(
677 "{}/storage/v1/upload/complete",
678 self.parent.base_url
679 );
680
681 let request_data = CompleteMultipartUploadRequest {
682 upload_id: upload_id.to_string(),
683 parts,
684 };
685
686 let params = [
687 ("bucket", self.bucket_id.to_string()),
688 ("key", path.to_string()), ];
690
691 let response = self.parent.http_client.post(&url)
692 .header("apikey", &self.parent.api_key)
693 .header("Content-Type", "application/json")
694 .json(&request_data)
695 .query(¶ms)
696 .send()
697 .await?;
698
699 if !response.status().is_success() {
700 let error_text = response.text().await?;
701 return Err(StorageError::ApiError(error_text));
702 }
703
704 let file_object: FileObject = response.json().await?;
705
706 Ok(file_object)
707 }
708
709 pub async fn abort_multipart_upload(
711 &self,
712 upload_id: &str,
713 path: &str
714 ) -> Result<()> {
715 let url = format!(
716 "{}/storage/v1/upload/abort",
717 self.parent.base_url
718 );
719
720 let payload = serde_json::json!({
721 "uploadId": upload_id,
722 "bucket": self.bucket_id,
723 "key": path,
724 });
725
726 let response = self.parent.http_client.post(&url)
727 .header("apikey", &self.parent.api_key)
728 .header("Content-Type", "application/json")
729 .json(&payload)
730 .send()
731 .await?;
732
733 if !response.status().is_success() {
734 let error_text = response.text().await?;
735 return Err(StorageError::ApiError(error_text));
736 }
737
738 Ok(())
739 }
740
741 pub async fn upload_large_file(
747 &self,
748 path: &str,
749 file_path: &Path,
750 chunk_size: usize,
751 options: Option<FileOptions>
752 ) -> Result<FileObject> {
753 let mut file = File::open(file_path).await?;
755
756 let file_size = file.metadata().await?.len() as usize;
758
759 let chunk_count = (file_size + chunk_size - 1) / chunk_size;
761
762 if chunk_count == 0 {
763 return Err(StorageError::new("File is empty".to_string()));
764 }
765
766 let init_response = self.initiate_multipart_upload(path, options).await?;
768
769 let mut uploaded_parts = Vec::with_capacity(chunk_count);
771
772 let mut buffer = vec![0u8; chunk_size];
774
775 for part_number in 1..=chunk_count as u32 {
777 let n = file.read(&mut buffer).await?;
779
780 if n == 0 {
781 break;
782 }
783
784 let chunk_data = Bytes::from(buffer[0..n].to_vec());
786
787 let part_info = self.upload_part(&init_response.upload_id, part_number, chunk_data).await?;
789
790 uploaded_parts.push(part_info);
792 }
793
794 let file_object = self.complete_multipart_upload(
796 &init_response.upload_id,
797 path,
798 uploaded_parts
799 ).await?;
800
801 Ok(file_object)
802 }
803
804 pub async fn transform_image(&self, path: &str, options: ImageTransformOptions) -> Result<Bytes> {
806 let url = format!(
807 "{}/storage/v1/object/transform/{}/{}{}",
808 self.parent.base_url,
809 self.bucket_id,
810 path,
811 options.to_query_params()
812 );
813
814 let response = self.parent.http_client.get(&url)
815 .header("apikey", &self.parent.api_key)
816 .send()
817 .await?;
818
819 if !response.status().is_success() {
820 let error_text = response.text().await?;
821 return Err(StorageError::ApiError(error_text));
822 }
823
824 let data = response.bytes().await?;
825
826 Ok(data)
827 }
828
829 pub fn get_public_transform_url(&self, path: &str, options: ImageTransformOptions) -> String {
831 format!(
832 "{}/storage/v1/object/public/transform/{}/{}{}",
833 self.parent.base_url,
834 self.bucket_id,
835 path,
836 options.to_query_params()
837 )
838 }
839
840 pub async fn create_signed_transform_url(
842 &self,
843 path: &str,
844 options: ImageTransformOptions,
845 expires_in: i32
846 ) -> Result<String> {
847 let transform_path = format!("transform/{}/{}{}", self.bucket_id, path, options.to_query_params());
848
849 let url = format!(
850 "{}/storage/v1/object/sign/{}",
851 self.parent.base_url,
852 transform_path,
853 );
854
855 let params = serde_json::json!({
856 "expiresIn": expires_in
857 });
858
859 let response = self.parent.http_client.post(&url)
860 .header("apikey", &self.parent.api_key)
861 .json(¶ms)
862 .send()
863 .await?;
864
865 if !response.status().is_success() {
866 let error_text = response.text().await?;
867 return Err(StorageError::ApiError(error_text));
868 }
869
870 #[derive(Deserialize)]
871 struct SignedUrlResponse {
872 signed_url: String,
873 }
874
875 let result = response.json::<SignedUrlResponse>().await?;
876
877 Ok(result.signed_url)
878 }
879
880 pub fn s3_compatible(&self, options: s3::S3Options) -> s3::S3BucketClient {
882 s3::S3BucketClient::new(
883 &self.parent.base_url,
884 &self.parent.api_key,
885 &self.bucket_id,
886 self.parent.http_client.clone(),
887 options
888 )
889 }
890}
891
892pub mod s3 {
894 use crate::StorageError;
895 use crate::Result;
896 use reqwest::Client;
897 use serde_json::Value;
898 use std::collections::HashMap;
899 use bytes::Bytes;
900 use serde::{Serialize, Deserialize};
901 use serde_json;
902
903 #[derive(Debug, Clone, Serialize, Deserialize)]
905 pub struct S3Options {
906 pub access_key_id: String,
908 pub secret_access_key: String,
910 #[serde(skip_serializing_if = "Option::is_none")]
912 pub region: Option<String>,
913 #[serde(skip_serializing_if = "Option::is_none")]
915 pub endpoint: Option<String>,
916 #[serde(skip_serializing_if = "Option::is_none")]
918 pub force_path_style: Option<bool>,
919 }
920
921 impl Default for S3Options {
922 fn default() -> Self {
923 Self {
924 access_key_id: String::new(),
925 secret_access_key: String::new(),
926 region: Some("auto".to_string()),
927 endpoint: None,
928 force_path_style: Some(true),
929 }
930 }
931 }
932
933 pub struct S3Client {
935 pub options: S3Options,
936 pub base_url: String,
937 pub api_key: String,
938 pub http_client: Client,
939 }
940
941 impl S3Client {
942 pub fn new(base_url: &str, api_key: &str, http_client: Client, options: S3Options) -> Self {
944 Self {
945 options,
946 base_url: base_url.to_string(),
947 api_key: api_key.to_string(),
948 http_client,
949 }
950 }
951
952 pub async fn create_bucket(&self, bucket_name: &str, is_public: bool) -> Result<()> {
954 let url = format!("{}/storage/v1/bucket", self.base_url);
955
956 let payload = serde_json::json!({
957 "name": bucket_name,
958 "public": is_public,
959 "file_size_limit": null,
960 "allowed_mime_types": null
961 });
962
963 let response = self.http_client
964 .post(&url)
965 .header("apikey", &self.api_key)
966 .header("Authorization", format!("Bearer {}", &self.api_key))
967 .json(&payload)
968 .send()
969 .await
970 .map_err(|e| StorageError::RequestError(e.to_string()))?;
971
972 if !response.status().is_success() {
973 let error_text = response.text().await
974 .unwrap_or_else(|_| "Unknown error".to_string());
975 return Err(StorageError::ApiError(error_text));
976 }
977
978 Ok(())
979 }
980
981 pub async fn delete_bucket(&self, bucket_name: &str) -> Result<()> {
983 let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_name);
984
985 let response = self.http_client
986 .delete(&url)
987 .header("apikey", &self.api_key)
988 .header("Authorization", format!("Bearer {}", &self.api_key))
989 .send()
990 .await
991 .map_err(|e| StorageError::RequestError(e.to_string()))?;
992
993 if !response.status().is_success() {
994 let error_text = response.text().await
995 .unwrap_or_else(|_| "Unknown error".to_string());
996 return Err(StorageError::ApiError(error_text));
997 }
998
999 Ok(())
1000 }
1001
1002 pub async fn list_buckets(&self) -> Result<Vec<serde_json::Value>> {
1004 let url = format!("{}/storage/v1/bucket", self.base_url);
1005
1006 let response = self.http_client
1007 .get(&url)
1008 .header("apikey", &self.api_key)
1009 .header("Authorization", format!("Bearer {}", &self.api_key))
1010 .send()
1011 .await
1012 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1013
1014 if !response.status().is_success() {
1015 let error_text = response.text().await
1016 .unwrap_or_else(|_| "Unknown error".to_string());
1017 return Err(StorageError::ApiError(error_text));
1018 }
1019
1020 let buckets = response.json::<Vec<serde_json::Value>>().await
1021 .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
1022
1023 Ok(buckets)
1024 }
1025
1026 pub fn bucket(&self, bucket_name: &str) -> S3BucketClient {
1028 S3BucketClient::new(
1029 &self.base_url,
1030 &self.api_key,
1031 bucket_name,
1032 self.http_client.clone(),
1033 self.options.clone()
1034 )
1035 }
1036 }
1037
1038 pub struct S3BucketClient {
1040 pub base_url: String,
1041 pub api_key: String,
1042 pub bucket_name: String,
1043 pub http_client: Client,
1044 pub options: S3Options,
1045 }
1046
1047 impl S3BucketClient {
1048 pub fn new(
1050 base_url: &str,
1051 api_key: &str,
1052 bucket_name: &str,
1053 http_client: Client,
1054 options: S3Options
1055 ) -> Self {
1056 Self {
1057 base_url: base_url.to_string(),
1058 api_key: api_key.to_string(),
1059 bucket_name: bucket_name.to_string(),
1060 http_client,
1061 options,
1062 }
1063 }
1064
1065 pub async fn put_object(&self, path: &str, data: Bytes, content_type: Option<String>, metadata: Option<HashMap<String, String>>) -> Result<()> {
1067 let url = format!(
1068 "{}/storage/v1/object/{}/{}",
1069 self.base_url,
1070 self.bucket_name,
1071 path.trim_start_matches('/')
1072 );
1073
1074 let content_type = content_type.unwrap_or_else(|| "application/octet-stream".to_string());
1075
1076 let mut request = self.http_client
1077 .put(&url)
1078 .header("apikey", &self.api_key)
1079 .header("Authorization", format!("Bearer {}", &self.api_key))
1080 .header("Content-Type", content_type)
1081 .body(data);
1082
1083 if let Some(metadata) = metadata {
1085 for (key, value) in metadata {
1086 request = request.header(&format!("x-amz-meta-{}", key), value);
1087 }
1088 }
1089
1090 let response = request
1091 .send()
1092 .await
1093 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1094
1095 if !response.status().is_success() {
1096 let error_text = response.text().await
1097 .unwrap_or_else(|_| "Unknown error".to_string());
1098 return Err(StorageError::ApiError(error_text));
1099 }
1100
1101 Ok(())
1102 }
1103
1104 pub async fn get_object(&self, path: &str) -> Result<Bytes> {
1106 let url = format!(
1107 "{}/storage/v1/object/{}/{}",
1108 self.base_url,
1109 self.bucket_name,
1110 path.trim_start_matches('/')
1111 );
1112
1113 let response = self.http_client
1114 .get(&url)
1115 .header("apikey", &self.api_key)
1116 .header("Authorization", format!("Bearer {}", &self.api_key))
1117 .send()
1118 .await
1119 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1120
1121 if !response.status().is_success() {
1122 let error_text = response.text().await
1123 .unwrap_or_else(|_| "Unknown error".to_string());
1124 return Err(StorageError::ApiError(error_text));
1125 }
1126
1127 let data = response.bytes().await
1128 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1129
1130 Ok(data)
1131 }
1132
1133 pub async fn head_object(&self, path: &str) -> Result<HashMap<String, String>> {
1135 let url = format!(
1136 "{}/storage/v1/object/{}/{}",
1137 self.base_url,
1138 self.bucket_name,
1139 path.trim_start_matches('/')
1140 );
1141
1142 let response = self.http_client
1143 .head(&url)
1144 .header("apikey", &self.api_key)
1145 .header("Authorization", format!("Bearer {}", &self.api_key))
1146 .send()
1147 .await
1148 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1149
1150 if !response.status().is_success() {
1151 return Err(StorageError::ApiError("Object not found".to_string()));
1152 }
1153
1154 let mut metadata = HashMap::new();
1155
1156 for (key, value) in response.headers() {
1158 let key_str = key.to_string();
1159 if key_str.starts_with("x-amz-meta-") {
1160 let meta_key = key_str.trim_start_matches("x-amz-meta-").to_string();
1161 metadata.insert(meta_key, value.to_str().unwrap_or_default().to_string());
1162 }
1163 }
1164
1165 Ok(metadata)
1166 }
1167
1168 pub async fn delete_object(&self, path: &str) -> Result<()> {
1170 let url = format!(
1171 "{}/storage/v1/object/{}/{}",
1172 self.base_url,
1173 self.bucket_name,
1174 path.trim_start_matches('/')
1175 );
1176
1177 let response = self.http_client
1178 .delete(&url)
1179 .header("apikey", &self.api_key)
1180 .header("Authorization", format!("Bearer {}", &self.api_key))
1181 .send()
1182 .await
1183 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1184
1185 if !response.status().is_success() {
1186 let error_text = response.text().await
1187 .unwrap_or_else(|_| "Unknown error".to_string());
1188 return Err(StorageError::ApiError(error_text));
1189 }
1190
1191 Ok(())
1192 }
1193
1194 pub async fn list_objects(&self, prefix: Option<&str>, delimiter: Option<&str>, max_keys: Option<i32>) -> Result<serde_json::Value> {
1196 let mut url = format!(
1197 "{}/storage/v1/object/list/{}",
1198 self.base_url,
1199 self.bucket_name
1200 );
1201
1202 let mut query_params = Vec::new();
1204
1205 if let Some(prefix) = prefix {
1206 query_params.push(format!("prefix={}", prefix));
1207 }
1208
1209 if let Some(delimiter) = delimiter {
1210 query_params.push(format!("delimiter={}", delimiter));
1211 }
1212
1213 if let Some(max_keys) = max_keys {
1214 query_params.push(format!("max-keys={}", max_keys));
1215 }
1216
1217 if !query_params.is_empty() {
1218 url = format!("{}?{}", url, query_params.join("&"));
1219 }
1220
1221 let response = self.http_client
1222 .get(&url)
1223 .header("apikey", &self.api_key)
1224 .header("Authorization", format!("Bearer {}", &self.api_key))
1225 .send()
1226 .await
1227 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1228
1229 if !response.status().is_success() {
1230 let error_text = response.text().await
1231 .unwrap_or_else(|_| "Unknown error".to_string());
1232 return Err(StorageError::ApiError(error_text));
1233 }
1234
1235 let objects = response.json::<serde_json::Value>().await
1236 .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
1237
1238 Ok(objects)
1239 }
1240
1241 pub async fn copy_object(&self, source_path: &str, destination_path: &str) -> Result<()> {
1243 let url = format!(
1244 "{}/storage/v1/object/copy",
1245 self.base_url
1246 );
1247
1248 let payload = serde_json::json!({
1249 "bucketId": self.bucket_name,
1250 "sourceKey": source_path,
1251 "destinationKey": destination_path
1252 });
1253
1254 let response = self.http_client
1255 .post(&url)
1256 .header("apikey", &self.api_key)
1257 .header("Authorization", format!("Bearer {}", &self.api_key))
1258 .json(&payload)
1259 .send()
1260 .await
1261 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1262
1263 if !response.status().is_success() {
1264 let error_text = response.text().await
1265 .unwrap_or_else(|_| "Unknown error".to_string());
1266 return Err(StorageError::ApiError(error_text));
1267 }
1268
1269 Ok(())
1270 }
1271 }
1272}
1273
1274#[cfg(test)]
1275mod tests {
1276 use super::*;
1277
1278 #[tokio::test]
1279 async fn test_list_buckets() {
1280 }
1282
1283 #[tokio::test]
1284 async fn test_multipart_upload() {
1285 }
1288
1289 #[tokio::test]
1290 async fn test_transform_image() {
1291 let mock_server = MockServer::start().await;
1292
1293 Mock::given(method("GET"))
1295 .and(path("/storage/v1/object/transform/test-bucket/image.jpg"))
1296 .and(query_param("width", "300"))
1297 .and(query_param("height", "200"))
1298 .and(query_param("format", "webp"))
1299 .respond_with(ResponseTemplate::new(200)
1300 .set_body_bytes(vec![0, 1, 2, 3, 4]) .append_header("Content-Type", "image/webp")
1302 )
1303 .mount(&mock_server)
1304 .await;
1305
1306 let http_client = reqwest::Client::new();
1307 let storage_client = StorageClient::new(&mock_server.uri(), "fake-key", http_client);
1308 let bucket_client = storage_client.from("test-bucket");
1309
1310 let options = ImageTransformOptions::new()
1311 .with_width(300)
1312 .with_height(200)
1313 .with_format("webp");
1314
1315 let result = bucket_client.transform_image("image.jpg", options).await;
1316
1317 assert!(result.is_ok());
1318 let image_data = result.unwrap();
1319 assert_eq!(image_data.len(), 5);
1320 }
1321
1322 #[tokio::test]
1323 async fn test_get_public_transform_url() {
1324 let http_client = reqwest::Client::new();
1325 let storage_client = StorageClient::new("https://example.com", "fake-key", http_client);
1326 let bucket_client = storage_client.from("test-bucket");
1327
1328 let options = ImageTransformOptions::new()
1329 .with_width(300)
1330 .with_height(200)
1331 .with_format("webp");
1332
1333 let url = bucket_client.get_public_transform_url("image.jpg", options);
1334
1335 assert!(url.contains("https://example.com/storage/v1/object/public/transform/test-bucket/image.jpg"));
1336 assert!(url.contains("width=300"));
1337 assert!(url.contains("height=200"));
1338 assert!(url.contains("format=webp"));
1339 }
1340
1341 #[tokio::test]
1342 async fn test_create_signed_transform_url() {
1343 let mock_server = MockServer::start().await;
1344
1345 Mock::given(method("POST"))
1347 .and(path("/storage/v1/object/sign/transform/test-bucket/image.jpg"))
1348 .respond_with(ResponseTemplate::new(200)
1349 .set_body_json(json!({
1350 "signed_url": "https://example.com/storage/v1/object/signed/transform/test-bucket/image.jpg?width=300&height=200&format=webp&token=abc123"
1351 }))
1352 )
1353 .mount(&mock_server)
1354 .await;
1355
1356 let http_client = reqwest::Client::new();
1357 let storage_client = StorageClient::new(&mock_server.uri(), "fake-key", http_client);
1358 let bucket_client = storage_client.from("test-bucket");
1359
1360 let options = ImageTransformOptions::new()
1361 .with_width(300)
1362 .with_height(200)
1363 .with_format("webp");
1364
1365 let result = bucket_client.create_signed_transform_url("image.jpg", options, 60).await;
1366
1367 assert!(result.is_ok());
1368 let url = result.unwrap();
1369 assert!(url.contains("https://example.com/storage/v1/object/signed/transform/test-bucket/image.jpg"));
1370 assert!(url.contains("token=abc123"));
1371 }
1372}