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}