1use bytes::Bytes;
7use reqwest::multipart::{Form, Part};
8use reqwest::Client;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::path::Path;
12use thiserror::Error;
13use tokio::fs::File;
14use tokio::io::AsyncReadExt;
15use url::Url;
16
17pub type Result<T> = std::result::Result<T, StorageError>;
19
20#[derive(Error, Debug)]
22pub enum StorageError {
23    #[error("API error: {0}")]
24    ApiError(String),
25
26    #[error("Network error: {0}")]
27    NetworkError(#[from] reqwest::Error),
28
29    #[error("JSON serialization error: {0}")]
30    SerializationError(#[from] serde_json::Error),
31
32    #[error("URL parse error: {0}")]
33    UrlParseError(#[from] url::ParseError),
34
35    #[error("Storage error: {0}")]
36    StorageError(String),
37
38    #[error("File not found: {0}")]
39    FileNotFound(String),
40
41    #[error("IO error: {0}")]
42    IoError(#[from] std::io::Error),
43
44    #[error("Request error: {0}")]
45    RequestError(String),
46
47    #[error("Deserialization error: {0}")]
48    DeserializationError(String),
49}
50
51impl StorageError {
52    pub fn new(message: String) -> Self {
53        Self::StorageError(message)
54    }
55}
56
57#[derive(Debug, Clone, Serialize, Default)]
59pub struct FileOptions {
60    pub cache_control: Option<String>,
61    pub content_type: Option<String>,
62    pub upsert: Option<bool>,
63}
64
65impl FileOptions {
66    pub fn new() -> Self {
68        Self::default()
69    }
70
71    pub fn with_cache_control(mut self, cache_control: &str) -> Self {
73        self.cache_control = Some(cache_control.to_string());
74        self
75    }
76
77    pub fn with_content_type(mut self, content_type: &str) -> Self {
79        self.content_type = Some(content_type.to_string());
80        self
81    }
82
83    pub fn with_upsert(mut self, upsert: bool) -> Self {
85        self.upsert = Some(upsert);
86        self
87    }
88}
89
90#[derive(Debug, Clone, Serialize, Default)]
92pub struct ListOptions {
93    pub limit: Option<i32>,
94    pub offset: Option<i32>,
95    pub sort_by: Option<SortBy>,
96    pub search: Option<String>,
97}
98
99impl ListOptions {
100    pub fn new() -> Self {
102        Self::default()
103    }
104
105    pub fn limit(mut self, limit: i32) -> Self {
107        self.limit = Some(limit);
108        self
109    }
110
111    pub fn offset(mut self, offset: i32) -> Self {
113        self.offset = Some(offset);
114        self
115    }
116
117    pub fn sort_by(mut self, column: &str, order: SortOrder) -> Self {
119        self.sort_by = Some(SortBy {
120            column: column.to_string(),
121            order,
122        });
123        self
124    }
125
126    pub fn search(mut self, search: &str) -> Self {
128        self.search = Some(search.to_string());
129        self
130    }
131}
132
133#[derive(Debug, Clone, Serialize)]
135pub struct SortBy {
136    pub column: String,
137    pub order: SortOrder,
138}
139
140impl std::fmt::Display for SortBy {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        write!(f, "{}:{:?}", self.column, self.order).map(|_| ())
143    }
144}
145
146#[derive(Debug, Clone, Copy, Serialize)]
148#[serde(rename_all = "lowercase")]
149pub enum SortOrder {
150    Asc,
151    Desc,
152}
153
154#[derive(Debug, Clone, Serialize, Default)]
156pub struct ImageTransformOptions {
157    pub width: Option<u32>,
158    pub height: Option<u32>,
159    pub resize: Option<String>,
160    pub format: Option<String>,
161    pub quality: Option<u32>,
162}
163
164impl ImageTransformOptions {
165    pub fn new() -> Self {
167        Self::default()
168    }
169
170    pub fn with_width(mut self, width: u32) -> Self {
172        self.width = Some(width);
173        self
174    }
175
176    pub fn with_height(mut self, height: u32) -> Self {
178        self.height = Some(height);
179        self
180    }
181
182    pub fn with_resize(mut self, resize: &str) -> Self {
184        self.resize = Some(resize.to_string());
185        self
186    }
187
188    pub fn with_format(mut self, format: &str) -> Self {
190        self.format = Some(format.to_string());
191        self
192    }
193
194    pub fn with_quality(mut self, quality: u32) -> Self {
196        self.quality = Some(quality.min(100));
197        self
198    }
199
200    fn to_query_params(&self) -> String {
202        let mut params = Vec::new();
203
204        if let Some(width) = self.width {
205            params.push(format!("width={}", width));
206        }
207
208        if let Some(height) = self.height {
209            params.push(format!("height={}", height));
210        }
211
212        if let Some(resize) = &self.resize {
213            params.push(format!("resize={}", resize));
214        }
215
216        if let Some(format) = &self.format {
217            params.push(format!("format={}", format));
218        }
219
220        if let Some(quality) = self.quality {
221            params.push(format!("quality={}", quality));
222        }
223
224        params.join("&")
225    }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct FileObject {
231    pub name: String,
232    pub bucket_id: String,
233    pub owner: String,
234    pub id: String,
235    pub updated_at: String,
236    pub created_at: String,
237    pub last_accessed_at: String,
238    pub metadata: Option<serde_json::Value>,
239    pub mime_type: Option<String>,
240    pub size: i64,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct Bucket {
246    pub id: String,
247    pub name: String,
248    pub owner: String,
249    pub public: bool,
250    pub created_at: String,
251    pub updated_at: String,
252}
253
254#[derive(Debug, Clone, Deserialize)]
256pub struct InitiateMultipartUploadResponse {
257    pub id: String,
258    #[serde(rename = "uploadId")]
259    pub upload_id: String,
260    pub key: String,
261    pub bucket: String,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct UploadedPartInfo {
267    #[serde(rename = "partNumber")]
268    pub part_number: u32,
269    pub etag: String,
270}
271
272#[derive(Debug, Clone, Serialize)]
274struct CompleteMultipartUploadRequest {
275    #[serde(rename = "uploadId")]
276    pub upload_id: String,
277    pub parts: Vec<UploadedPartInfo>,
278}
279
280pub struct StorageBucketClient<'a> {
282    parent: &'a StorageClient,
283    bucket_id: String,
284}
285
286pub struct StorageClient {
288    base_url: String,
289    api_key: String,
290    http_client: Client,
291}
292
293impl StorageClient {
294    pub fn new(base_url: &str, api_key: &str, http_client: Client) -> Self {
296        Self {
297            base_url: base_url.to_string(),
298            api_key: api_key.to_string(),
299            http_client,
300        }
301    }
302
303    pub fn from<'a>(&'a self, bucket_id: &str) -> StorageBucketClient<'a> {
305        StorageBucketClient {
306            parent: self,
307            bucket_id: bucket_id.to_string(),
308        }
309    }
310
311    pub async fn list_buckets(&self) -> Result<Vec<Bucket>> {
313        let url = format!("{}/storage/v1/bucket", self.base_url);
314
315        let response = self
316            .http_client
317            .get(&url)
318            .header("apikey", &self.api_key)
319            .send()
320            .await?;
321
322        if !response.status().is_success() {
323            let error_text = response.text().await?;
324            return Err(StorageError::ApiError(error_text));
325        }
326
327        let buckets = response.json::<Vec<Bucket>>().await?;
328
329        Ok(buckets)
330    }
331
332    pub async fn create_bucket(&self, bucket_id: &str, is_public: bool) -> Result<Bucket> {
334        let url = format!("{}/storage/v1/bucket", self.base_url);
335
336        let payload = serde_json::json!({
337            "id": bucket_id,
338            "name": bucket_id,
339            "public": is_public
340        });
341
342        let response = self
343            .http_client
344            .post(&url)
345            .header("apikey", &self.api_key)
346            .header("Content-Type", "application/json")
347            .json(&payload)
348            .send()
349            .await?;
350
351        if !response.status().is_success() {
352            let error_text = response.text().await?;
353            return Err(StorageError::ApiError(error_text));
354        }
355
356        let bucket = response.json::<Bucket>().await?;
357
358        Ok(bucket)
359    }
360
361    pub async fn delete_bucket(&self, bucket_id: &str) -> Result<()> {
363        let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_id);
364
365        let response = self
366            .http_client
367            .delete(&url)
368            .header("apikey", &self.api_key)
369            .send()
370            .await?;
371
372        if !response.status().is_success() {
373            let error_text = response.text().await?;
374            return Err(StorageError::ApiError(error_text));
375        }
376
377        Ok(())
378    }
379
380    pub async fn update_bucket(&self, bucket_id: &str, is_public: bool) -> Result<Bucket> {
382        let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_id);
383
384        let payload = serde_json::json!({
385            "id": bucket_id,
386            "public": is_public
387        });
388
389        let response = self
390            .http_client
391            .put(&url)
392            .header("apikey", &self.api_key)
393            .header("Content-Type", "application/json")
394            .json(&payload)
395            .send()
396            .await?;
397
398        if !response.status().is_success() {
399            let error_text = response.text().await?;
400            return Err(StorageError::ApiError(error_text));
401        }
402
403        let bucket = response.json::<Bucket>().await?;
404
405        Ok(bucket)
406    }
407}
408
409impl<'a> StorageBucketClient<'a> {
410    pub async fn upload(
412        &self,
413        path: &str,
414        file_path: &Path,
415        options: Option<FileOptions>,
416    ) -> Result<FileObject> {
417        let mut url = Url::parse(&self.parent.base_url)?;
418        url.set_path(&format!("/storage/v1/object/{}/{}", self.bucket_id, path));
419
420        if let Some(opts) = &options {
422            let mut query_pairs = url.query_pairs_mut();
423            if let Some(cache_control) = &opts.cache_control {
424                query_pairs.append_pair("cache_control", cache_control);
425            }
426            if let Some(upsert) = &opts.upsert {
427                query_pairs.append_pair("upsert", &upsert.to_string());
428            }
429        }
430
431        let mut file = File::open(file_path).await?;
433        let mut contents = Vec::new();
434        file.read_to_end(&mut contents).await?;
435
436        let part = Part::bytes(contents)
438            .file_name(file_path.file_name().unwrap().to_string_lossy().to_string());
439
440        let form = Form::new().part("file", part);
441
442        let response = self
443            .parent
444            .http_client
445            .post(url)
446            .header("apikey", &self.parent.api_key)
447            .header("Authorization", format!("Bearer {}", &self.parent.api_key))
448            .multipart(form)
449            .send()
450            .await?;
451
452        if !response.status().is_success() {
453            let error_text = response.text().await?;
454            return Err(StorageError::ApiError(error_text));
455        }
456
457        let file_object = response.json::<FileObject>().await?;
458
459        Ok(file_object)
460    }
461
462    pub async fn download(&self, path: &str) -> Result<Bytes> {
464        let mut url = Url::parse(&self.parent.base_url)?;
465        url.set_path(&format!("/storage/v1/object/{}/{}", self.bucket_id, path));
466
467        let response = self
468            .parent
469            .http_client
470            .get(url)
471            .header("apikey", &self.parent.api_key)
472            .header("Authorization", format!("Bearer {}", &self.parent.api_key))
473            .send()
474            .await?;
475
476        if !response.status().is_success() {
477            let error_text = response.text().await?;
478            return Err(StorageError::ApiError(error_text));
479        }
480
481        let bytes = response.bytes().await?;
482
483        Ok(bytes)
484    }
485
486    pub async fn list(
488        &self,
489        prefix: &str,
490        options: Option<ListOptions>,
491    ) -> Result<Vec<FileObject>> {
492        let mut url = Url::parse(&self.parent.base_url)?;
493        url.set_path(&format!("/storage/v1/object/list/{}", self.bucket_id));
494
495        {
497            let mut query_pairs = url.query_pairs_mut();
498            query_pairs.append_pair("prefix", prefix);
499
500            if let Some(opts) = &options {
501                if let Some(limit) = opts.limit {
502                    query_pairs.append_pair("limit", &limit.to_string());
503                }
504                if let Some(offset) = opts.offset {
505                    query_pairs.append_pair("offset", &offset.to_string());
506                }
507                if let Some(sort_by) = &opts.sort_by {
508                    query_pairs.append_pair("sortBy", &sort_by.to_string());
509                }
510                if let Some(search) = &opts.search {
511                    query_pairs.append_pair("search", search);
512                }
513            }
514        } let response = self
517            .parent
518            .http_client
519            .get(url)
520            .header("apikey", &self.parent.api_key)
521            .header("Authorization", format!("Bearer {}", &self.parent.api_key))
522            .send()
523            .await?;
524
525        if !response.status().is_success() {
526            let error_text = response.text().await?;
527            return Err(StorageError::ApiError(error_text));
528        }
529
530        let files = response.json::<Vec<FileObject>>().await?;
531
532        Ok(files)
533    }
534
535    pub async fn remove(&self, paths: Vec<&str>) -> Result<()> {
537        let url = format!(
538            "{}/storage/v1/object/{}",
539            self.parent.base_url, self.bucket_id
540        );
541
542        let payload = serde_json::json!({
543            "prefixes": paths
544        });
545
546        let response = self
547            .parent
548            .http_client
549            .delete(&url)
550            .header("apikey", &self.parent.api_key)
551            .header("Content-Type", "application/json")
552            .json(&payload)
553            .send()
554            .await?;
555
556        if !response.status().is_success() {
557            let error_text = response.text().await?;
558            return Err(StorageError::ApiError(error_text));
559        }
560
561        Ok(())
562    }
563
564    pub fn get_public_url(&self, path: &str) -> String {
566        format!(
567            "{}/storage/v1/object/public/{}/{}",
568            self.parent.base_url, self.bucket_id, path
569        )
570    }
571
572    pub async fn create_signed_url(&self, path: &str, expires_in: i32) -> Result<String> {
574        let url = format!(
575            "{}/storage/v1/object/sign/{}/{}",
576            self.parent.base_url, self.bucket_id, path
577        );
578
579        let payload = serde_json::json!({
580            "expiresIn": expires_in
581        });
582
583        let response = self
584            .parent
585            .http_client
586            .post(&url)
587            .header("apikey", &self.parent.api_key)
588            .header("Content-Type", "application/json")
589            .json(&payload)
590            .send()
591            .await?;
592
593        if !response.status().is_success() {
594            let error_text = response.text().await?;
595            return Err(StorageError::ApiError(error_text));
596        }
597
598        #[derive(Deserialize)]
599        struct SignedUrlResponse {
600            signed_url: String,
601        }
602
603        let signed_url = response.json::<SignedUrlResponse>().await?;
604
605        Ok(signed_url.signed_url)
606    }
607
608    pub async fn initiate_multipart_upload(
610        &self,
611        path: &str,
612        options: Option<FileOptions>,
613    ) -> Result<InitiateMultipartUploadResponse> {
614        let url = format!("{}/storage/v1/upload/initiate", self.parent.base_url);
615
616        let options = options.unwrap_or_default();
617
618        let cache_control = options
619            .cache_control
620            .unwrap_or_else(|| "max-age=3600".to_string());
621        let content_type = options
622            .content_type
623            .unwrap_or_else(|| "application/octet-stream".to_string());
624        let upsert = options.upsert.unwrap_or(false);
625
626        let payload = serde_json::json!({
627            "bucket": self.bucket_id,
628            "name": path,
629            "cacheControl": cache_control,
630            "contentType": content_type,
631            "upsert": upsert,
632        });
633
634        let response = self
635            .parent
636            .http_client
637            .post(&url)
638            .header("apikey", &self.parent.api_key)
639            .header("Content-Type", "application/json")
640            .json(&payload)
641            .send()
642            .await?;
643
644        if !response.status().is_success() {
645            let error_text = response.text().await?;
646            return Err(StorageError::ApiError(error_text));
647        }
648
649        let initiate_response: InitiateMultipartUploadResponse = response.json().await?;
650
651        Ok(initiate_response)
652    }
653
654    pub async fn upload_part(
656        &self,
657        upload_id: &str,
658        part_number: u32,
659        data: Bytes,
660    ) -> Result<UploadedPartInfo> {
661        let url = format!("{}/storage/v1/upload/part", self.parent.base_url);
662
663        let body = reqwest::Body::from(data);
664
665        let response = self
666            .parent
667            .http_client
668            .post(&url)
669            .header("apikey", &self.parent.api_key)
670            .query(&[
671                ("uploadId", upload_id),
672                ("partNumber", &part_number.to_string()),
673                ("bucket", &self.bucket_id),
674            ])
675            .body(body)
676            .send()
677            .await?;
678
679        if !response.status().is_success() {
680            let error_text = response.text().await?;
681            return Err(StorageError::ApiError(error_text));
682        }
683
684        let etag = response
685            .headers()
686            .get("etag")
687            .ok_or_else(|| StorageError::new("ETag header not found in response".to_string()))?
688            .to_str()
689            .map_err(|e| StorageError::new(format!("Invalid ETag header: {}", e)))?
690            .to_string();
691
692        let part_info = UploadedPartInfo { part_number, etag };
693
694        Ok(part_info)
695    }
696
697    pub async fn complete_multipart_upload(
699        &self,
700        upload_id: &str,
701        path: &str,
702        parts: Vec<UploadedPartInfo>,
703    ) -> Result<FileObject> {
704        let url = format!("{}/storage/v1/upload/complete", self.parent.base_url);
705
706        let payload = CompleteMultipartUploadRequest {
707            upload_id: upload_id.to_string(),
708            parts,
709        };
710
711        let response = self
712            .parent
713            .http_client
714            .post(&url)
715            .header("apikey", &self.parent.api_key)
716            .header("Content-Type", "application/json")
717            .query(&[("bucket", &self.bucket_id), ("key", &path.to_string())])
718            .json(&payload)
719            .send()
720            .await
721            .map_err(StorageError::NetworkError)?;
722
723        if !response.status().is_success() {
724            let error_text = response.text().await?;
725            return Err(StorageError::ApiError(error_text));
726        }
727
728        let file_object: FileObject = response.json().await?;
729
730        Ok(file_object)
731    }
732
733    pub async fn abort_multipart_upload(&self, upload_id: &str, path: &str) -> Result<()> {
735        let url = format!("{}/storage/v1/upload/abort", self.parent.base_url);
736
737        let payload = serde_json::json!({
738            "uploadId": upload_id,
739            "bucket": self.bucket_id,
740            "key": path,
741        });
742
743        let response = self
744            .parent
745            .http_client
746            .post(&url)
747            .header("apikey", &self.parent.api_key)
748            .header("Content-Type", "application/json")
749            .json(&payload)
750            .send()
751            .await?;
752
753        if !response.status().is_success() {
754            let error_text = response.text().await?;
755            return Err(StorageError::ApiError(error_text));
756        }
757
758        Ok(())
759    }
760
761    pub async fn upload_large_file(
767        &self,
768        path: &str,
769        file_path: &Path,
770        chunk_size: usize,
771        options: Option<FileOptions>,
772    ) -> Result<FileObject> {
773        let mut file = File::open(file_path).await?;
775
776        let file_size = file.metadata().await?.len() as usize;
778
779        let chunk_count = file_size.div_ceil(chunk_size);
781
782        if chunk_count == 0 {
783            return Err(StorageError::new("File is empty".to_string()));
784        }
785
786        let init_response = self.initiate_multipart_upload(path, options).await?;
788
789        let mut uploaded_parts = Vec::with_capacity(chunk_count);
791
792        let mut buffer = vec![0u8; chunk_size];
794
795        for part_number in 1..=chunk_count as u32 {
797            let n = file.read(&mut buffer).await?;
799
800            if n == 0 {
801                break;
802            }
803
804            let chunk_data = Bytes::from(buffer[0..n].to_vec());
806
807            let part_info = self
809                .upload_part(&init_response.upload_id, part_number, chunk_data)
810                .await?;
811
812            uploaded_parts.push(part_info);
814        }
815
816        let file_object = self
818            .complete_multipart_upload(&init_response.upload_id, path, uploaded_parts)
819            .await?;
820
821        Ok(file_object)
822    }
823
824    pub async fn transform_image(
826        &self,
827        path: &str,
828        options: ImageTransformOptions,
829    ) -> Result<Bytes> {
830        let url = format!(
831            "{}/object/transform/authenticated/{}/{}",
832            self.parent.base_url, self.bucket_id, path
833        );
834
835        let query_params = options.to_query_params();
837        let request_url = if query_params.is_empty() {
838            url
839        } else {
840            format!("{}?{}", url, query_params)
841        };
842
843        let res = self
844            .parent
845            .http_client
846            .get(&request_url)
847            .header("apikey", &self.parent.api_key)
848            .header("Authorization", format!("Bearer {}", self.parent.api_key))
849            .send()
850            .await
851            .map_err(StorageError::NetworkError)?;
852
853        let status = res.status();
855
856        if !status.is_success() {
857            let error_text = res
858                .text()
859                .await
860                .unwrap_or_else(|_| "Unknown error".to_string());
861            return Err(StorageError::ApiError(format!(
862                "Failed to transform image: {} (Status: {})",
863                error_text, status
864            )));
865        }
866
867        let bytes = res.bytes().await.map_err(StorageError::NetworkError)?;
868        Ok(bytes)
869    }
870
871    pub fn get_public_transform_url(&self, path: &str, options: ImageTransformOptions) -> String {
873        let base_url = format!(
874            "{}/object/public/{}/{}",
875            self.parent.base_url, self.bucket_id, path
876        );
877
878        let query_params = options.to_query_params();
880        if query_params.is_empty() {
881            base_url
882        } else {
883            format!("{}?{}", base_url, query_params)
884        }
885    }
886
887    pub async fn create_signed_transform_url(
889        &self,
890        path: &str,
891        options: ImageTransformOptions,
892        expires_in: i32,
893    ) -> Result<String> {
894        let url = format!(
895            "{}/object/sign/{}/{}",
896            self.parent.base_url, self.bucket_id, path
897        );
898
899        let transform_params = options.to_query_params();
901
902        let payload = json!({
903            "expiresIn": expires_in,
904            "transform": transform_params,
905        });
906
907        let res = self
908            .parent
909            .http_client
910            .post(&url)
911            .header("apikey", &self.parent.api_key)
912            .header("Authorization", format!("Bearer {}", self.parent.api_key))
913            .json(&payload)
914            .send()
915            .await
916            .map_err(StorageError::NetworkError)?;
917
918        let status = res.status();
920
921        if !status.is_success() {
922            let error_text = res
923                .text()
924                .await
925                .unwrap_or_else(|_| "Unknown error".to_string());
926            return Err(StorageError::ApiError(format!(
927                "Failed to create signed transform URL: {} (Status: {})",
928                error_text, status
929            )));
930        }
931
932        #[derive(Debug, Deserialize)]
933        struct SignedUrlResponse {
934            signed_url: String,
935        }
936
937        let response = res
938            .json::<SignedUrlResponse>()
939            .await
940            .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
941
942        Ok(response.signed_url)
943    }
944
945    pub fn s3_compatible(&self, options: s3::S3Options) -> s3::S3BucketClient {
947        s3::S3BucketClient::new(
948            &self.parent.base_url,
949            &self.parent.api_key,
950            &self.bucket_id,
951            self.parent.http_client.clone(),
952            options,
953        )
954    }
955
956    pub async fn move_object(&self, source_path: &str, destination_path: &str) -> Result<()> {
967        let url_str = format!("{}/object/move", self.parent.base_url);
968        let url = Url::parse(&url_str).map_err(StorageError::UrlParseError)?;
969
970        let body = json!({
971            "bucketId": self.bucket_id,
972            "sourceKey": source_path,
973            "destinationKey": destination_path
974        });
975
976        let response = self
977            .parent
978            .http_client
979            .post(url)
980            .header("apikey", &self.parent.api_key)
981            .header("Authorization", format!("Bearer {}", self.parent.api_key))
982            .header("Content-Type", "application/json")
983            .json(&body)
984            .send()
985            .await
986            .map_err(StorageError::NetworkError)?;
987
988        if response.status().is_success() {
989            Ok(())
990        } else {
991            let status = response.status();
992            let error_text = response
993                .text()
994                .await
995                .unwrap_or_else(|_| "Failed to read error body".to_string());
996            let error_message =
998                if let Ok(json_err) = serde_json::from_str::<serde_json::Value>(&error_text) {
999                    json_err
1000                        .get("message")
1001                        .and_then(|v| v.as_str())
1002                        .unwrap_or(&error_text)
1003                        .to_string()
1004                } else {
1005                    error_text
1006                };
1007            Err(StorageError::ApiError(format!(
1008                "Failed to move object: {} (Status: {})",
1009                error_message, status
1010            )))
1011        }
1012    }
1013}
1014
1015pub mod s3 {
1017    use crate::{Result, StorageError};
1018    use bytes::Bytes;
1019    use reqwest::Client;
1020    use serde::{Deserialize, Serialize};
1021    use std::collections::HashMap;
1022
1023    #[derive(Debug, Clone, Serialize, Deserialize)]
1025    pub struct S3Options {
1026        pub access_key_id: String,
1028        pub secret_access_key: String,
1030        #[serde(skip_serializing_if = "Option::is_none")]
1032        pub region: Option<String>,
1033        #[serde(skip_serializing_if = "Option::is_none")]
1035        pub endpoint: Option<String>,
1036        #[serde(skip_serializing_if = "Option::is_none")]
1038        pub force_path_style: Option<bool>,
1039    }
1040
1041    impl Default for S3Options {
1042        fn default() -> Self {
1043            Self {
1044                access_key_id: String::new(),
1045                secret_access_key: String::new(),
1046                region: Some("auto".to_string()),
1047                endpoint: None,
1048                force_path_style: Some(true),
1049            }
1050        }
1051    }
1052
1053    pub struct S3Client {
1055        pub options: S3Options,
1056        pub base_url: String,
1057        pub api_key: String,
1058        pub http_client: Client,
1059    }
1060
1061    impl S3Client {
1062        pub fn new(base_url: &str, api_key: &str, http_client: Client, options: S3Options) -> Self {
1064            Self {
1065                options,
1066                base_url: base_url.to_string(),
1067                api_key: api_key.to_string(),
1068                http_client,
1069            }
1070        }
1071
1072        pub async fn create_bucket(&self, bucket_name: &str, is_public: bool) -> Result<()> {
1074            let url = format!("{}/storage/v1/bucket", self.base_url);
1075
1076            let payload = serde_json::json!({
1077                "name": bucket_name,
1078                "public": is_public,
1079                "file_size_limit": null,
1080                "allowed_mime_types": null
1081            });
1082
1083            let response = self
1084                .http_client
1085                .post(&url)
1086                .header("apikey", &self.api_key)
1087                .header("Authorization", format!("Bearer {}", &self.api_key))
1088                .json(&payload)
1089                .send()
1090                .await
1091                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1092
1093            if !response.status().is_success() {
1094                let error_text = response
1095                    .text()
1096                    .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 delete_bucket(&self, bucket_name: &str) -> Result<()> {
1106            let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_name);
1107
1108            let response = self
1109                .http_client
1110                .delete(&url)
1111                .header("apikey", &self.api_key)
1112                .header("Authorization", format!("Bearer {}", &self.api_key))
1113                .send()
1114                .await
1115                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1116
1117            if !response.status().is_success() {
1118                let error_text = response
1119                    .text()
1120                    .await
1121                    .unwrap_or_else(|_| "Unknown error".to_string());
1122                return Err(StorageError::ApiError(error_text));
1123            }
1124
1125            Ok(())
1126        }
1127
1128        pub async fn list_buckets(&self) -> Result<Vec<serde_json::Value>> {
1130            let url = format!("{}/storage/v1/bucket", self.base_url);
1131
1132            let response = self
1133                .http_client
1134                .get(&url)
1135                .header("apikey", &self.api_key)
1136                .header("Authorization", format!("Bearer {}", &self.api_key))
1137                .send()
1138                .await
1139                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1140
1141            if !response.status().is_success() {
1142                let error_text = response
1143                    .text()
1144                    .await
1145                    .unwrap_or_else(|_| "Unknown error".to_string());
1146                return Err(StorageError::ApiError(error_text));
1147            }
1148
1149            let buckets = response
1150                .json::<Vec<serde_json::Value>>()
1151                .await
1152                .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
1153
1154            Ok(buckets)
1155        }
1156
1157        pub fn bucket(&self, bucket_name: &str) -> S3BucketClient {
1159            S3BucketClient::new(
1160                &self.base_url,
1161                &self.api_key,
1162                bucket_name,
1163                self.http_client.clone(),
1164                self.options.clone(),
1165            )
1166        }
1167    }
1168
1169    pub struct S3BucketClient {
1171        pub base_url: String,
1172        pub api_key: String,
1173        pub bucket_name: String,
1174        pub http_client: Client,
1175        pub options: S3Options,
1176    }
1177
1178    impl S3BucketClient {
1179        pub fn new(
1181            base_url: &str,
1182            api_key: &str,
1183            bucket_name: &str,
1184            http_client: Client,
1185            options: S3Options,
1186        ) -> Self {
1187            Self {
1188                base_url: base_url.to_string(),
1189                api_key: api_key.to_string(),
1190                bucket_name: bucket_name.to_string(),
1191                http_client,
1192                options,
1193            }
1194        }
1195
1196        pub async fn put_object(
1198            &self,
1199            path: &str,
1200            data: Bytes,
1201            content_type: Option<String>,
1202            metadata: Option<HashMap<String, String>>,
1203        ) -> Result<()> {
1204            let url = format!(
1205                "{}/storage/v1/object/{}/{}",
1206                self.base_url,
1207                self.bucket_name,
1208                path.trim_start_matches('/')
1209            );
1210
1211            let content_type =
1212                content_type.unwrap_or_else(|| "application/octet-stream".to_string());
1213
1214            let mut request = self
1215                .http_client
1216                .put(&url)
1217                .header("apikey", &self.api_key)
1218                .header("Authorization", format!("Bearer {}", &self.api_key))
1219                .header("Content-Type", content_type)
1220                .body(data);
1221
1222            if let Some(metadata) = metadata {
1224                for (key, value) in metadata {
1225                    request = request.header(&format!("x-amz-meta-{}", key), value);
1226                }
1227            }
1228
1229            let response = request
1230                .send()
1231                .await
1232                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1233
1234            if !response.status().is_success() {
1235                let error_text = response
1236                    .text()
1237                    .await
1238                    .unwrap_or_else(|_| "Unknown error".to_string());
1239                return Err(StorageError::ApiError(error_text));
1240            }
1241
1242            Ok(())
1243        }
1244
1245        pub async fn get_object(&self, path: &str) -> Result<Bytes> {
1247            let url = format!(
1248                "{}/storage/v1/object/{}/{}",
1249                self.base_url,
1250                self.bucket_name,
1251                path.trim_start_matches('/')
1252            );
1253
1254            let response = self
1255                .http_client
1256                .get(&url)
1257                .header("apikey", &self.api_key)
1258                .header("Authorization", format!("Bearer {}", &self.api_key))
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
1265                    .text()
1266                    .await
1267                    .unwrap_or_else(|_| "Unknown error".to_string());
1268                return Err(StorageError::ApiError(error_text));
1269            }
1270
1271            let data = response
1272                .bytes()
1273                .await
1274                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1275
1276            Ok(data)
1277        }
1278
1279        pub async fn head_object(&self, path: &str) -> Result<HashMap<String, String>> {
1281            let url = format!(
1282                "{}/storage/v1/object/{}/{}",
1283                self.base_url,
1284                self.bucket_name,
1285                path.trim_start_matches('/')
1286            );
1287
1288            let response = self
1289                .http_client
1290                .head(&url)
1291                .header("apikey", &self.api_key)
1292                .header("Authorization", format!("Bearer {}", &self.api_key))
1293                .send()
1294                .await
1295                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1296
1297            if !response.status().is_success() {
1298                return Err(StorageError::ApiError("Object not found".to_string()));
1299            }
1300
1301            let mut metadata = HashMap::new();
1302
1303            for (key, value) in response.headers() {
1305                let key_str = key.to_string();
1306                if key_str.starts_with("x-amz-meta-") {
1307                    let meta_key = key_str.trim_start_matches("x-amz-meta-").to_string();
1308                    metadata.insert(meta_key, value.to_str().unwrap_or_default().to_string());
1309                }
1310            }
1311
1312            Ok(metadata)
1313        }
1314
1315        pub async fn delete_object(&self, path: &str) -> Result<()> {
1317            let url = format!(
1318                "{}/storage/v1/object/{}/{}",
1319                self.base_url,
1320                self.bucket_name,
1321                path.trim_start_matches('/')
1322            );
1323
1324            let response = self
1325                .http_client
1326                .delete(&url)
1327                .header("apikey", &self.api_key)
1328                .header("Authorization", format!("Bearer {}", &self.api_key))
1329                .send()
1330                .await
1331                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1332
1333            if !response.status().is_success() {
1334                let error_text = response
1335                    .text()
1336                    .await
1337                    .unwrap_or_else(|_| "Unknown error".to_string());
1338                return Err(StorageError::ApiError(error_text));
1339            }
1340
1341            Ok(())
1342        }
1343
1344        pub async fn list_objects(
1346            &self,
1347            prefix: Option<&str>,
1348            delimiter: Option<&str>,
1349            max_keys: Option<i32>,
1350        ) -> Result<serde_json::Value> {
1351            let mut url = format!(
1352                "{}/storage/v1/object/list/{}",
1353                self.base_url, self.bucket_name
1354            );
1355
1356            let mut query_params = Vec::new();
1358
1359            if let Some(prefix) = prefix {
1360                query_params.push(format!("prefix={}", prefix));
1361            }
1362
1363            if let Some(delimiter) = delimiter {
1364                query_params.push(format!("delimiter={}", delimiter));
1365            }
1366
1367            if let Some(max_keys) = max_keys {
1368                query_params.push(format!("max-keys={}", max_keys));
1369            }
1370
1371            if !query_params.is_empty() {
1372                url = format!("{}?{}", url, query_params.join("&"));
1373            }
1374
1375            let response = self
1376                .http_client
1377                .get(&url)
1378                .header("apikey", &self.api_key)
1379                .header("Authorization", format!("Bearer {}", &self.api_key))
1380                .send()
1381                .await
1382                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1383
1384            if !response.status().is_success() {
1385                let error_text = response
1386                    .text()
1387                    .await
1388                    .unwrap_or_else(|_| "Unknown error".to_string());
1389                return Err(StorageError::ApiError(error_text));
1390            }
1391
1392            let objects = response
1393                .json::<serde_json::Value>()
1394                .await
1395                .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
1396
1397            Ok(objects)
1398        }
1399
1400        pub async fn copy_object(&self, source_path: &str, destination_path: &str) -> Result<()> {
1402            let url = format!("{}/storage/v1/object/copy", self.base_url);
1403
1404            let payload = serde_json::json!({
1405                "bucketId": self.bucket_name,
1406                "sourceKey": source_path,
1407                "destinationKey": destination_path
1408            });
1409
1410            let response = self
1411                .http_client
1412                .post(&url)
1413                .header("apikey", &self.api_key)
1414                .header("Authorization", format!("Bearer {}", &self.api_key))
1415                .json(&payload)
1416                .send()
1417                .await
1418                .map_err(|e| StorageError::RequestError(e.to_string()))?;
1419
1420            if !response.status().is_success() {
1421                let error_text = response
1422                    .text()
1423                    .await
1424                    .unwrap_or_else(|_| "Unknown error".to_string());
1425                return Err(StorageError::ApiError(error_text));
1426            }
1427
1428            Ok(())
1429        }
1430    }
1431}
1432
1433#[cfg(test)]
1434mod tests {
1435    use super::*;
1436    use serde_json::json;
1437    use wiremock::matchers::{method, path};
1438    use wiremock::{Mock, MockServer, ResponseTemplate};
1439
1440    #[tokio::test]
1441    async fn test_list_buckets() {
1442        let mock_server = MockServer::start().await;
1444
1445        let buckets_response = json!([
1447            {
1448                "id": "bucket1",
1449                "name": "bucket1",
1450                "owner": "owner-uuid",
1451                "public": false,
1452                "created_at": "2024-01-01T00:00:00Z",
1453                "updated_at": "2024-01-01T00:00:00Z"
1454            },
1455            {
1456                "id": "bucket2",
1457                "name": "bucket2",
1458                "owner": "owner-uuid",
1459                "public": true,
1460                "created_at": "2024-01-02T00:00:00Z",
1461                "updated_at": "2024-01-02T00:00:00Z"
1462            }
1463        ]);
1464        Mock::given(method("GET"))
1465            .and(path("/storage/v1/bucket"))
1466            .respond_with(ResponseTemplate::new(200).set_body_json(buckets_response.clone()))
1467            .mount(&mock_server)
1468            .await;
1469
1470        let http_client = reqwest::Client::new();
1472        let storage_client =
1473            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1474
1475        let result = storage_client.list_buckets().await;
1477        assert!(result.is_ok());
1478        let buckets = result.unwrap();
1479        assert_eq!(buckets.len(), 2);
1480        assert_eq!(buckets[0].id, "bucket1");
1481        assert!(buckets[1].public);
1482
1483        mock_server.reset().await;
1485
1486        let error_response = json!({ "message": "Unauthorized" });
1488        Mock::given(method("GET"))
1489            .and(path("/storage/v1/bucket"))
1490            .respond_with(ResponseTemplate::new(401).set_body_json(error_response))
1491            .mount(&mock_server)
1492            .await;
1493
1494        let result = storage_client.list_buckets().await;
1496        assert!(result.is_err());
1497        if let Err(StorageError::ApiError(msg)) = result {
1498            assert!(msg.contains("Unauthorized"));
1499        } else {
1500            panic!("Expected ApiError, got {:?}", result);
1501        }
1502    }
1503
1504    #[tokio::test]
1505    async fn test_create_bucket() {
1506        let mock_server = MockServer::start().await;
1508
1509        let bucket_id = "new-bucket";
1511        let request_body = json!({ "id": bucket_id, "name": bucket_id, "public": true });
1512        let response_body = json!({
1513            "id": bucket_id,
1514            "name": bucket_id,
1515            "owner": "owner-uuid",
1516            "public": true,
1517            "created_at": "2024-01-03T00:00:00Z",
1518            "updated_at": "2024-01-03T00:00:00Z"
1519        });
1520
1521        Mock::given(method("POST"))
1522            .and(path("/storage/v1/bucket"))
1523            .and(wiremock::matchers::body_json(request_body.clone())) .respond_with(ResponseTemplate::new(200).set_body_json(response_body.clone()))
1525            .mount(&mock_server)
1526            .await;
1527
1528        let http_client = reqwest::Client::new();
1530        let storage_client =
1531            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1532
1533        let result = storage_client.create_bucket(bucket_id, true).await;
1535        assert!(result.is_ok(), "create_bucket failed: {:?}", result.err());
1536        let bucket = result.unwrap();
1537        assert_eq!(bucket.id, bucket_id);
1538        assert_eq!(bucket.name, bucket_id);
1539        assert!(bucket.public);
1540
1541        mock_server.reset().await;
1543
1544        let error_response = json!({ "message": "Bucket already exists" });
1546        Mock::given(method("POST"))
1547            .and(path("/storage/v1/bucket"))
1548            .and(wiremock::matchers::body_json(request_body.clone()))
1549            .respond_with(ResponseTemplate::new(409).set_body_json(error_response))
1550            .mount(&mock_server)
1551            .await;
1552
1553        let result = storage_client.create_bucket(bucket_id, true).await;
1555        assert!(result.is_err());
1556        if let Err(StorageError::ApiError(msg)) = result {
1557            assert!(msg.contains("Bucket already exists"));
1558        } else {
1559            panic!("Expected ApiError, got {:?}", result);
1560        }
1561    }
1562
1563    #[tokio::test]
1564    async fn test_delete_bucket() {
1565        let mock_server = MockServer::start().await;
1567        let bucket_id = "bucket-to-delete";
1568
1569        Mock::given(method("DELETE"))
1571            .and(path(format!("/storage/v1/bucket/{}", bucket_id)))
1572            .respond_with(
1573                ResponseTemplate::new(200)
1574                    .set_body_json(json!({ "message": "Successfully deleted" })),
1575            )
1576            .mount(&mock_server)
1577            .await;
1578
1579        let http_client = reqwest::Client::new();
1581        let storage_client =
1582            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1583
1584        let result = storage_client.delete_bucket(bucket_id).await;
1586        assert!(result.is_ok(), "delete_bucket failed: {:?}", result.err());
1587
1588        mock_server.reset().await;
1590
1591        let error_response = json!({ "message": "Bucket not found" });
1593        Mock::given(method("DELETE"))
1594            .and(path(format!("/storage/v1/bucket/{}", bucket_id)))
1595            .respond_with(ResponseTemplate::new(404).set_body_json(error_response))
1596            .mount(&mock_server)
1597            .await;
1598
1599        let result = storage_client.delete_bucket(bucket_id).await;
1601        assert!(result.is_err());
1602        if let Err(StorageError::ApiError(msg)) = result {
1603            assert!(msg.contains("Bucket not found"));
1604        } else {
1605            panic!("Expected ApiError, got {:?}", result);
1606        }
1607    }
1608
1609    #[tokio::test]
1610    async fn test_update_bucket() {
1611        let mock_server = MockServer::start().await;
1613        let bucket_id = "bucket-to-update";
1614        let updated_public_status = false;
1615
1616        let request_body = json!({ "id": bucket_id, "public": updated_public_status });
1618        let response_body = json!({
1619            "id": bucket_id,
1620            "name": bucket_id,
1621            "owner": "owner-uuid",
1622            "public": updated_public_status, "created_at": "2024-01-04T00:00:00Z",
1624            "updated_at": "2024-01-04T01:00:00Z"
1625        });
1626
1627        Mock::given(method("PUT"))
1628            .and(path(format!("/storage/v1/bucket/{}", bucket_id)))
1629            .and(wiremock::matchers::body_json(request_body.clone()))
1630            .respond_with(ResponseTemplate::new(200).set_body_json(response_body.clone()))
1631            .mount(&mock_server)
1632            .await;
1633
1634        let http_client = reqwest::Client::new();
1636        let storage_client =
1637            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1638
1639        let result = storage_client
1641            .update_bucket(bucket_id, updated_public_status)
1642            .await;
1643        assert!(result.is_ok(), "update_bucket failed: {:?}", result.err());
1644        let bucket = result.unwrap();
1645        assert_eq!(bucket.id, bucket_id);
1646        assert_eq!(bucket.public, updated_public_status);
1647
1648        mock_server.reset().await;
1650
1651        let error_response = json!({ "message": "Bucket not found for update" });
1653        Mock::given(method("PUT"))
1654            .and(path(format!("/storage/v1/bucket/{}", bucket_id)))
1655            .and(wiremock::matchers::body_json(request_body.clone())) .respond_with(ResponseTemplate::new(404).set_body_json(error_response))
1657            .mount(&mock_server)
1658            .await;
1659
1660        let result = storage_client
1662            .update_bucket(bucket_id, updated_public_status)
1663            .await;
1664        assert!(result.is_err());
1665        if let Err(StorageError::ApiError(msg)) = result {
1666            assert!(msg.contains("Bucket not found for update"));
1667        } else {
1668            panic!("Expected ApiError, got {:?}", result);
1669        }
1670    }
1671
1672    #[tokio::test]
1673    async fn test_upload_file() {
1674        let mock_server = MockServer::start().await;
1676        let bucket_id = "upload-bucket";
1677        let object_path = "test_file.txt";
1678        let file_content = "Hello, Supabase Storage!";
1679
1680        let temp_dir = tempfile::tempdir().unwrap();
1682        let file_path = temp_dir.path().join(object_path);
1683        tokio::fs::write(&file_path, file_content).await.unwrap();
1684
1685        let response_body = json!({
1687            "name": object_path,
1688            "bucket_id": bucket_id,
1689            "owner": "owner-uuid",
1690            "id": "file-id-upload",
1691            "updated_at": "2024-01-05T00:00:00Z",
1692            "created_at": "2024-01-05T00:00:00Z",
1693            "last_accessed_at": "2024-01-05T00:00:00Z",
1694            "metadata": { "size": file_content.len(), "mimetype": "text/plain" },
1695            "size": file_content.len(),
1696            "mime_type": "text/plain",
1697        });
1698        Mock::given(method("POST"))
1701            .and(path(format!(
1702                "/storage/v1/object/{}/{}",
1703                bucket_id, object_path
1704            )))
1705            .respond_with(ResponseTemplate::new(200).set_body_json(response_body.clone()))
1706            .mount(&mock_server)
1707            .await;
1708
1709        let http_client = reqwest::Client::new();
1711        let storage_client =
1712            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1713        let bucket_client = storage_client.from(bucket_id);
1714
1715        let result = bucket_client.upload(object_path, &file_path, None).await;
1717        assert!(result.is_ok(), "upload failed: {:?}", result.err());
1718        let file_object = result.unwrap();
1719        assert_eq!(file_object.name, object_path);
1720        assert_eq!(file_object.size, file_content.len() as i64);
1721
1722        mock_server.reset().await;
1724
1725        let error_response = json!({ "message": "Invalid upload parameters" });
1727        Mock::given(method("POST"))
1728            .and(path(format!(
1729                "/storage/v1/object/{}/{}",
1730                bucket_id, object_path
1731            )))
1732            .respond_with(ResponseTemplate::new(400).set_body_json(error_response))
1733            .mount(&mock_server)
1734            .await;
1735
1736        let result = bucket_client.upload(object_path, &file_path, None).await;
1738        assert!(result.is_err());
1739        if let Err(StorageError::ApiError(msg)) = result {
1740            assert!(msg.contains("Invalid upload parameters"));
1741        } else {
1742            panic!("Expected ApiError, got {:?}", result);
1743        }
1744
1745        }
1747
1748    #[tokio::test]
1749    async fn test_download_file() {
1750        let mock_server = MockServer::start().await;
1752        let bucket_id = "download-bucket";
1753        let object_path = "download_me.txt";
1754        let file_content = Bytes::from_static(b"File content to download");
1755
1756        Mock::given(method("GET"))
1758            .and(path(format!(
1759                "/storage/v1/object/{}/{}",
1760                bucket_id, object_path
1761            )))
1762            .respond_with(ResponseTemplate::new(200).set_body_bytes(file_content.clone()))
1763            .mount(&mock_server)
1764            .await;
1765
1766        let http_client = reqwest::Client::new();
1768        let storage_client =
1769            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1770        let bucket_client = storage_client.from(bucket_id);
1771
1772        let result = bucket_client.download(object_path).await;
1774        assert!(result.is_ok(), "download failed: {:?}", result.err());
1775        let downloaded_content = result.unwrap();
1776        assert_eq!(downloaded_content, file_content);
1777
1778        mock_server.reset().await;
1780
1781        let error_response = json!({ "message": "File not found" });
1783        Mock::given(method("GET"))
1784            .and(path(format!(
1785                "/storage/v1/object/{}/{}",
1786                bucket_id, object_path
1787            )))
1788            .respond_with(ResponseTemplate::new(404).set_body_json(error_response))
1789            .mount(&mock_server)
1790            .await;
1791
1792        let result = bucket_client.download(object_path).await;
1794        assert!(result.is_err());
1795        if let Err(StorageError::ApiError(msg)) = result {
1796            assert!(msg.contains("File not found"));
1797        } else {
1798            panic!("Expected ApiError, got {:?}", result);
1799        }
1800    }
1801
1802    #[tokio::test]
1803    async fn test_list_files() {
1804        let mock_server = MockServer::start().await;
1806        let bucket_id = "list-bucket";
1807        let prefix = "folder/";
1808
1809        let list_options = ListOptions::new()
1811            .limit(10)
1812            .offset(0)
1813            .sort_by("name", SortOrder::Asc);
1814        let response_body = json!([
1815            {
1816                "name": "folder/file1.txt",
1817                "id": "uuid1",
1818                "updated_at": "2024-01-05T00:00:00Z",
1819                "created_at": "2024-01-05T00:00:00Z",
1820                "last_accessed_at": "2024-01-05T00:00:00Z",
1821                "metadata": { "size": 100, "mimetype": "text/plain" },
1822                "bucket_id": bucket_id,
1823                "owner": "owner-uuid",
1824                "size": 100,
1825                "mime_type": "text/plain",
1826            },
1827            {
1828                "name": "folder/file2.png",
1829                "id": "uuid2",
1830                "updated_at": "2024-01-05T01:00:00Z",
1831                "created_at": "2024-01-05T01:00:00Z",
1832                "last_accessed_at": "2024-01-05T01:00:00Z",
1833                "metadata": { "size": 2048, "mimetype": "image/png" },
1834                "bucket_id": bucket_id,
1835                "owner": "owner-uuid",
1836                "size": 2048,
1837                "mime_type": "image/png",
1838            }
1839        ]);
1840
1841        Mock::given(method("GET")) .and(path(format!("/storage/v1/object/list/{}", bucket_id)))
1844            .and(wiremock::matchers::query_param("prefix", prefix))
1846            .and(wiremock::matchers::query_param("limit", "10"))
1847            .and(wiremock::matchers::query_param("offset", "0"))
1848            .and(wiremock::matchers::query_param(
1849                "sortBy", "name:Asc", ))
1851            .respond_with(ResponseTemplate::new(200).set_body_json(response_body.clone()))
1854            .mount(&mock_server)
1855            .await;
1856
1857        let http_client = reqwest::Client::new();
1859        let storage_client =
1860            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1861        let bucket_client = storage_client.from(bucket_id);
1862
1863        let result = bucket_client.list(prefix, Some(list_options)).await;
1865        assert!(result.is_ok(), "list failed: {:?}", result.err());
1866        let files = result.unwrap();
1867        assert_eq!(files.len(), 2);
1868        assert_eq!(files[0].name, "folder/file1.txt");
1869        assert_eq!(files[1].mime_type, Some("image/png".to_string()));
1870
1871        mock_server.reset().await;
1873
1874        let error_response = json!({ "message": "Invalid list parameters" });
1876        Mock::given(method("GET")) .and(path(format!("/storage/v1/object/list/{}", bucket_id)))
1879            .and(wiremock::matchers::query_param("prefix", prefix)) .respond_with(ResponseTemplate::new(400).set_body_json(error_response))
1883            .mount(&mock_server)
1884            .await;
1885
1886        let result = bucket_client.list(prefix, Some(ListOptions::new())).await; assert!(result.is_err());
1889        if let Err(StorageError::ApiError(msg)) = result {
1890            assert!(msg.contains("Invalid list parameters"));
1891        } else {
1892            panic!("Expected ApiError, got {:?}", result);
1893        }
1894    }
1895
1896    #[tokio::test]
1897    async fn test_remove_files() {
1898        let mock_server = MockServer::start().await;
1900        let bucket_id = "remove-bucket";
1901        let paths_to_remove = vec!["file_a.txt", "folder/file_b.log"];
1902
1903        let request_body = json!({ "prefixes": paths_to_remove });
1905        let response_body = json!([]); Mock::given(method("DELETE"))
1908            .and(path(format!("/storage/v1/object/{}", bucket_id)))
1909            .and(wiremock::matchers::body_json(request_body.clone()))
1910            .respond_with(ResponseTemplate::new(200).set_body_json(response_body.clone()))
1911            .mount(&mock_server)
1912            .await;
1913
1914        let http_client = reqwest::Client::new();
1916        let storage_client =
1917            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1918        let bucket_client = storage_client.from(bucket_id);
1919
1920        let result = bucket_client.remove(paths_to_remove).await;
1922        assert!(result.is_ok(), "remove failed: {:?}", result.err());
1923
1924        mock_server.reset().await;
1926
1927        let error_response = json!({ "message": "Invalid paths provided" });
1929        Mock::given(method("DELETE"))
1930            .and(path(format!("/storage/v1/object/{}", bucket_id)))
1931            .and(wiremock::matchers::body_json(request_body.clone())) .respond_with(ResponseTemplate::new(400).set_body_json(error_response))
1933            .mount(&mock_server)
1934            .await;
1935
1936        let paths_for_error = vec!["file_a.txt", "folder/file_b.log"]; let result = bucket_client.remove(paths_for_error).await;
1939        assert!(result.is_err());
1940        if let Err(StorageError::ApiError(msg)) = result {
1941            assert!(msg.contains("Invalid paths provided"));
1942        } else {
1943            panic!("Expected ApiError, got {:?}", result);
1944        }
1945    }
1946
1947    #[tokio::test]
1948    async fn test_create_signed_url() {
1949        let mock_server = MockServer::start().await;
1951        let bucket_id = "signed-url-bucket";
1952        let object_path = "private/doc.pdf";
1953        let expires_in = 3600;
1954        let expected_signed_url = format!(
1955            "{}/storage/v1/object/sign/{}/{}?token=test-token",
1956            mock_server.uri(),
1957            bucket_id,
1958            object_path
1959        );
1960
1961        let request_body = json!({ "expiresIn": expires_in });
1963        let response_body = json!({ "signed_url": expected_signed_url }); Mock::given(method("POST"))
1966            .and(path(format!(
1967                "/storage/v1/object/sign/{}/{}",
1968                bucket_id, object_path
1969            )))
1970            .and(wiremock::matchers::body_json(request_body.clone()))
1971            .respond_with(ResponseTemplate::new(200).set_body_json(response_body.clone()))
1972            .mount(&mock_server)
1973            .await;
1974
1975        let http_client = reqwest::Client::new();
1977        let storage_client =
1978            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
1979        let bucket_client = storage_client.from(bucket_id);
1980
1981        let result = bucket_client
1983            .create_signed_url(object_path, expires_in)
1984            .await;
1985        assert!(
1986            result.is_ok(),
1987            "create_signed_url failed: {:?}",
1988            result.err()
1989        );
1990        let signed_url = result.unwrap();
1991        assert!(signed_url.contains(&format!(
1993            "/storage/v1/object/sign/{}/{}",
1994            bucket_id, object_path
1995        )));
1996
1997        mock_server.reset().await;
1999
2000        let error_response = json!({ "message": "Object not found" });
2002        Mock::given(method("POST"))
2003            .and(path(format!(
2004                "/storage/v1/object/sign/{}/{}",
2005                bucket_id, object_path
2006            )))
2007            .and(wiremock::matchers::body_json(request_body.clone()))
2008            .respond_with(ResponseTemplate::new(404).set_body_json(error_response))
2009            .mount(&mock_server)
2010            .await;
2011
2012        let result = bucket_client
2014            .create_signed_url(object_path, expires_in)
2015            .await;
2016        assert!(result.is_err());
2017        if let Err(StorageError::ApiError(msg)) = result {
2018            assert!(msg.contains("Object not found"));
2019        } else {
2020            panic!("Expected ApiError, got {:?}", result);
2021        }
2022    }
2023
2024    #[tokio::test]
2025    async fn test_get_public_url() {
2026        let http_client = reqwest::Client::new();
2027        let storage_client =
2028            StorageClient::new("https://test.supabase.co", "anon-key", http_client);
2029        let bucket_client = storage_client.from("public-images");
2030        let object_path = "logos/supabase.png";
2031
2032        let public_url = bucket_client.get_public_url(object_path);
2033
2034        assert_eq!(
2035            public_url,
2036            "https://test.supabase.co/storage/v1/object/public/public-images/logos/supabase.png"
2037        );
2038    }
2039
2040    #[tokio::test]
2041    async fn test_multipart_upload() {
2042        let mock_server = MockServer::start().await;
2044        let bucket_id = "multipart-bucket";
2045        let object_path = "large_file.dat";
2046        let file_content = Bytes::from_static(b"Part1ContentPart2MoreContent");
2048        let chunk_size = 10; let temp_dir = tempfile::tempdir().unwrap();
2052        let file_path = temp_dir.path().join(object_path);
2053        tokio::fs::write(&file_path, &file_content).await.unwrap();
2054
2055        let upload_id = "test-upload-id-complex";
2056
2057        let initiate_request_body = json!({
2059            "bucket": bucket_id,
2060            "name": object_path,
2061            "cacheControl": "max-age=3600",
2062            "contentType": "application/octet-stream",
2063            "upsert": false
2064        });
2065        let initiate_response_body = json!({
2066            "id": "file-id-multi",
2067            "uploadId": upload_id,
2068            "key": object_path,
2069            "bucket": bucket_id
2070        });
2071        Mock::given(method("POST"))
2072            .and(path("/storage/v1/upload/initiate"))
2073            .and(wiremock::matchers::body_json(initiate_request_body))
2074            .respond_with(ResponseTemplate::new(200).set_body_json(initiate_response_body))
2075            .mount(&mock_server)
2076            .await;
2077
2078        let etag1 = "etag-part-1";
2080        Mock::given(method("POST"))
2081            .and(path("/storage/v1/upload/part"))
2082            .and(wiremock::matchers::query_param("uploadId", upload_id))
2083            .and(wiremock::matchers::query_param("partNumber", "1"))
2084            .and(wiremock::matchers::query_param("bucket", bucket_id))
2085            .respond_with(ResponseTemplate::new(200).insert_header("ETag", etag1))
2086            .mount(&mock_server)
2087            .await;
2088
2089        let etag2 = "etag-part-2";
2090        Mock::given(method("POST"))
2091            .and(path("/storage/v1/upload/part"))
2092            .and(wiremock::matchers::query_param("uploadId", upload_id))
2093            .and(wiremock::matchers::query_param("partNumber", "2"))
2094            .and(wiremock::matchers::query_param("bucket", bucket_id))
2095            .respond_with(ResponseTemplate::new(200).insert_header("ETag", etag2))
2096            .mount(&mock_server)
2097            .await;
2098
2099        let etag3 = "etag-part-3"; Mock::given(method("POST"))
2101            .and(path("/storage/v1/upload/part"))
2102            .and(wiremock::matchers::query_param("uploadId", upload_id))
2103            .and(wiremock::matchers::query_param("partNumber", "3"))
2104            .and(wiremock::matchers::query_param("bucket", bucket_id))
2105            .respond_with(ResponseTemplate::new(200).insert_header("ETag", etag3))
2106            .mount(&mock_server)
2107            .await;
2108
2109        let expected_parts = vec![
2111            json!({ "partNumber": 1, "etag": etag1 }),
2112            json!({ "partNumber": 2, "etag": etag2 }),
2113            json!({ "partNumber": 3, "etag": etag3 }), ];
2115        let complete_request_body = json!({
2116            "uploadId": upload_id,
2117            "parts": expected_parts
2118        });
2119        let complete_response_body = json!({
2120            "name": object_path,
2121            "bucket_id": bucket_id,
2122            "owner": "owner-uuid",
2123            "id": "file-id-multi-complete",
2124            "updated_at": "2024-01-07T00:00:00Z",
2125            "created_at": "2024-01-07T00:00:00Z",
2126            "last_accessed_at": "2024-01-07T00:00:00Z",
2127            "metadata": { "size": file_content.len(), "mimetype": "application/octet-stream" },
2128            "size": file_content.len(),
2129            "mime_type": "application/octet-stream",
2130        });
2131        Mock::given(method("POST"))
2132            .and(path("/storage/v1/upload/complete"))
2133            .and(wiremock::matchers::query_param("bucket", bucket_id))
2134            .and(wiremock::matchers::query_param("key", object_path))
2135            .and(wiremock::matchers::body_json(complete_request_body))
2136            .respond_with(ResponseTemplate::new(200).set_body_json(complete_response_body))
2137            .mount(&mock_server)
2138            .await;
2139
2140        let http_client = reqwest::Client::new();
2142        let storage_client =
2143            StorageClient::new(&mock_server.uri(), "fake-key", http_client.clone());
2144        let bucket_client = storage_client.from(bucket_id);
2145
2146        let result = bucket_client
2148            .upload_large_file(object_path, &file_path, chunk_size, None)
2149            .await;
2150        assert!(
2151            result.is_ok(),
2152            "upload_large_file failed: {:?}",
2153            result.err()
2154        );
2155        let file_object = result.unwrap();
2156        assert_eq!(file_object.name, object_path);
2157        assert_eq!(file_object.size, file_content.len() as i64);
2158    }
2159
2160    #[tokio::test]
2161    async fn test_transform_image() {
2162        let mock_server = MockServer::start().await;
2164        let bucket_id = "transform-bucket";
2165        let object_path = "images/logo_to_transform.png";
2166        let transform_options = ImageTransformOptions::new()
2167            .with_width(100)
2168            .with_height(100)
2169            .with_resize("contain")
2170            .with_format("webp");
2171        let expected_image_bytes = Bytes::from_static(b"transformed_image_data");
2172
2173        let http_client = reqwest::Client::new();
2175        let storage_client = StorageClient::new(&mock_server.uri(), "fake-anon-key", http_client);
2176        let bucket_client = storage_client.from(bucket_id);
2177
2178        Mock::given(method("GET"))
2180            .and(path(format!(
2181                "/object/transform/authenticated/{}/{}",
2182                bucket_id, object_path
2183            )))
2184            .and(wiremock::matchers::query_param("width", "100"))
2185            .and(wiremock::matchers::query_param("height", "100"))
2186            .and(wiremock::matchers::query_param("resize", "contain"))
2187            .and(wiremock::matchers::query_param("format", "webp"))
2188            .respond_with(ResponseTemplate::new(200).set_body_bytes(expected_image_bytes.clone()))
2189            .mount(&mock_server)
2190            .await;
2191
2192        let result = bucket_client
2194            .transform_image(object_path, transform_options.clone())
2195            .await;
2196        assert!(result.is_ok(), "transform_image failed: {:?}", result.err());
2197        let image_bytes = result.unwrap();
2198        assert_eq!(image_bytes, expected_image_bytes);
2199
2200        mock_server.reset().await;
2202
2203        let error_response = json!({ "statusCode": "400", "error": "BadRequest", "message": "Invalid transform options" });
2205        Mock::given(method("GET"))
2206            .and(path(format!(
2207                "/object/transform/authenticated/{}/{}",
2208                bucket_id, object_path
2209            )))
2210            .respond_with(ResponseTemplate::new(400).set_body_json(error_response))
2212            .mount(&mock_server)
2213            .await;
2214
2215        let result = bucket_client
2217            .transform_image(object_path, transform_options) .await;
2219        assert!(result.is_err());
2220        if let Err(StorageError::ApiError(msg)) = result {
2221            assert!(msg.contains("Invalid transform options") || msg.contains("BadRequest"));
2222        } else {
2223            panic!("Expected ApiError, got {:?}", result);
2224        }
2225    }
2226
2227    #[tokio::test]
2228    async fn test_create_signed_transform_url() {
2229        let mock_server = MockServer::start().await;
2232        let bucket_id = "transform-bucket";
2233        let object_path = "images/logo.png";
2234        let expires_in = 3600; let transform_options = ImageTransformOptions::new()
2236            .with_width(50)
2237            .with_height(50)
2238            .with_resize("cover")
2239            .with_format("jpeg")
2240            .with_quality(80);
2241
2242        let http_client = reqwest::Client::new();
2244        let storage_client = StorageClient::new(&mock_server.uri(), "fake-anon-key", http_client);
2245        let bucket_client = storage_client.from(bucket_id);
2246
2247        let expected_transform_string = transform_options.to_query_params();
2249        let expected_request_body = json!({
2250            "expiresIn": expires_in,
2251            "transform": expected_transform_string });
2253        let expected_signed_url = format!(
2254            "{}/storage/v1/object/sign/{}/{}?token=test-token&transform={}",
2255            mock_server.uri(),
2256            bucket_id,
2257            object_path,
2258            expected_transform_string
2259        );
2260        let response_body = json!({ "signed_url": expected_signed_url });
2261
2262        Mock::given(method("POST"))
2263            .and(path(format!(
2264                "/object/sign/{}/{}", bucket_id, object_path
2266            )))
2267            .and(wiremock::matchers::body_json(expected_request_body.clone()))
2268            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
2269            .mount(&mock_server)
2270            .await;
2271
2272        let result = bucket_client
2274            .create_signed_transform_url(object_path, transform_options.clone(), expires_in)
2275            .await;
2276        assert!(
2277            result.is_ok(),
2278            "create_signed_transform_url failed: {:?}",
2279            result.err()
2280        );
2281        let signed_url = result.unwrap();
2282        assert!(signed_url.contains(&format!(
2284            "/storage/v1/object/sign/{}/{}",
2285            bucket_id, object_path
2286        )));
2287        assert!(signed_url.contains("token=test-token")); assert!(signed_url.contains(&expected_transform_string));
2289
2290        mock_server.reset().await;
2292
2293        let error_response = json!({ "statusCode": "400", "error": "BadRequest", "message": "Invalid transform parameters" });
2295        let expected_request_body_err = json!({
2296            "expiresIn": expires_in,
2297            "transform": expected_transform_string
2298        });
2299
2300        Mock::given(method("POST"))
2301            .and(path(format!(
2302                "/object/sign/{}/{}", bucket_id, object_path
2304            )))
2305            .and(wiremock::matchers::body_json(
2306                expected_request_body_err.clone(),
2307            ))
2308            .respond_with(ResponseTemplate::new(400).set_body_json(error_response))
2309            .mount(&mock_server)
2310            .await;
2311
2312        let result = bucket_client
2314            .create_signed_transform_url(object_path, transform_options, expires_in) .await;
2316        assert!(result.is_err());
2317        if let Err(StorageError::ApiError(msg)) = result {
2318            assert!(
2320                msg.contains("Invalid transform parameters") || msg.contains("BadRequest") );
2322        } else {
2323            panic!("Expected ApiError, got {:?}", result);
2324        }
2325    }
2326}