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 if params.is_empty() {
225 String::new()
226 } else {
227 format!("?{}", params.join("&"))
228 }
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct FileObject {
235 pub name: String,
236 pub bucket_id: String,
237 pub owner: String,
238 pub id: String,
239 pub updated_at: String,
240 pub created_at: String,
241 pub last_accessed_at: String,
242 pub metadata: Option<serde_json::Value>,
243 pub mime_type: Option<String>,
244 pub size: i64,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct Bucket {
250 pub id: String,
251 pub name: String,
252 pub owner: String,
253 pub public: bool,
254 pub created_at: String,
255 pub updated_at: String,
256}
257
258#[derive(Debug, Clone, Deserialize)]
260pub struct InitiateMultipartUploadResponse {
261 pub id: String,
262 #[serde(rename = "uploadId")]
263 pub upload_id: String,
264 pub key: String,
265 pub bucket: String,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct UploadedPartInfo {
271 #[serde(rename = "partNumber")]
272 pub part_number: u32,
273 pub etag: String,
274}
275
276#[derive(Debug, Clone, Serialize)]
278struct CompleteMultipartUploadRequest {
279 #[serde(rename = "uploadId")]
280 pub upload_id: String,
281 pub parts: Vec<UploadedPartInfo>,
282}
283
284pub struct StorageBucketClient<'a> {
286 parent: &'a StorageClient,
287 bucket_id: String,
288}
289
290pub struct StorageClient {
292 base_url: String,
293 api_key: String,
294 http_client: Client,
295}
296
297impl StorageClient {
298 pub fn new(base_url: &str, api_key: &str, http_client: Client) -> Self {
300 Self {
301 base_url: base_url.to_string(),
302 api_key: api_key.to_string(),
303 http_client,
304 }
305 }
306
307 pub fn from<'a>(&'a self, bucket_id: &str) -> StorageBucketClient<'a> {
309 StorageBucketClient {
310 parent: self,
311 bucket_id: bucket_id.to_string(),
312 }
313 }
314
315 pub async fn list_buckets(&self) -> Result<Vec<Bucket>> {
317 let url = format!("{}/storage/v1/bucket", self.base_url);
318
319 let response = self
320 .http_client
321 .get(&url)
322 .header("apikey", &self.api_key)
323 .send()
324 .await?;
325
326 if !response.status().is_success() {
327 let error_text = response.text().await?;
328 return Err(StorageError::ApiError(error_text));
329 }
330
331 let buckets = response.json::<Vec<Bucket>>().await?;
332
333 Ok(buckets)
334 }
335
336 pub async fn create_bucket(&self, bucket_id: &str, is_public: bool) -> Result<Bucket> {
338 let url = format!("{}/storage/v1/bucket", self.base_url);
339
340 let payload = serde_json::json!({
341 "id": bucket_id,
342 "name": bucket_id,
343 "public": is_public
344 });
345
346 let response = self
347 .http_client
348 .post(&url)
349 .header("apikey", &self.api_key)
350 .header("Content-Type", "application/json")
351 .json(&payload)
352 .send()
353 .await?;
354
355 if !response.status().is_success() {
356 let error_text = response.text().await?;
357 return Err(StorageError::ApiError(error_text));
358 }
359
360 let bucket = response.json::<Bucket>().await?;
361
362 Ok(bucket)
363 }
364
365 pub async fn delete_bucket(&self, bucket_id: &str) -> Result<()> {
367 let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_id);
368
369 let response = self
370 .http_client
371 .delete(&url)
372 .header("apikey", &self.api_key)
373 .send()
374 .await?;
375
376 if !response.status().is_success() {
377 let error_text = response.text().await?;
378 return Err(StorageError::ApiError(error_text));
379 }
380
381 Ok(())
382 }
383
384 pub async fn update_bucket(&self, bucket_id: &str, is_public: bool) -> Result<Bucket> {
386 let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_id);
387
388 let payload = serde_json::json!({
389 "id": bucket_id,
390 "public": is_public
391 });
392
393 let response = self
394 .http_client
395 .put(&url)
396 .header("apikey", &self.api_key)
397 .header("Content-Type", "application/json")
398 .json(&payload)
399 .send()
400 .await?;
401
402 if !response.status().is_success() {
403 let error_text = response.text().await?;
404 return Err(StorageError::ApiError(error_text));
405 }
406
407 let bucket = response.json::<Bucket>().await?;
408
409 Ok(bucket)
410 }
411}
412
413impl<'a> StorageBucketClient<'a> {
414 pub async fn upload(
416 &self,
417 path: &str,
418 file_path: &Path,
419 options: Option<FileOptions>,
420 ) -> Result<FileObject> {
421 let mut url = Url::parse(&self.parent.base_url)?;
422 url.set_path(&format!("/storage/v1/object/{}/{}", self.bucket_id, path));
423
424 if let Some(opts) = &options {
426 let mut query_pairs = url.query_pairs_mut();
427 if let Some(cache_control) = &opts.cache_control {
428 query_pairs.append_pair("cache_control", cache_control);
429 }
430 if let Some(upsert) = &opts.upsert {
431 query_pairs.append_pair("upsert", &upsert.to_string());
432 }
433 }
434
435 let mut file = File::open(file_path).await?;
437 let mut contents = Vec::new();
438 file.read_to_end(&mut contents).await?;
439
440 let part = Part::bytes(contents)
442 .file_name(file_path.file_name().unwrap().to_string_lossy().to_string());
443
444 let form = Form::new().part("file", part);
445
446 let response = self
447 .parent
448 .http_client
449 .post(url)
450 .header("apikey", &self.parent.api_key)
451 .header("Authorization", format!("Bearer {}", &self.parent.api_key))
452 .multipart(form)
453 .send()
454 .await?;
455
456 if !response.status().is_success() {
457 let error_text = response.text().await?;
458 return Err(StorageError::ApiError(error_text));
459 }
460
461 let file_object = response.json::<FileObject>().await?;
462
463 Ok(file_object)
464 }
465
466 pub async fn download(&self, path: &str) -> Result<Bytes> {
468 let mut url = Url::parse(&self.parent.base_url)?;
469 url.set_path(&format!("/storage/v1/object/{}/{}", self.bucket_id, path));
470
471 let response = self
472 .parent
473 .http_client
474 .get(url)
475 .header("apikey", &self.parent.api_key)
476 .header("Authorization", format!("Bearer {}", &self.parent.api_key))
477 .send()
478 .await?;
479
480 if !response.status().is_success() {
481 let error_text = response.text().await?;
482 return Err(StorageError::ApiError(error_text));
483 }
484
485 let bytes = response.bytes().await?;
486
487 Ok(bytes)
488 }
489
490 pub async fn list(
492 &self,
493 prefix: &str,
494 options: Option<ListOptions>,
495 ) -> Result<Vec<FileObject>> {
496 let mut url = Url::parse(&self.parent.base_url)?;
497 url.set_path(&format!("/storage/v1/object/list/{}", self.bucket_id));
498
499 {
501 let mut query_pairs = url.query_pairs_mut();
502 query_pairs.append_pair("prefix", prefix);
503
504 if let Some(opts) = &options {
505 if let Some(limit) = opts.limit {
506 query_pairs.append_pair("limit", &limit.to_string());
507 }
508 if let Some(offset) = opts.offset {
509 query_pairs.append_pair("offset", &offset.to_string());
510 }
511 if let Some(sort_by) = &opts.sort_by {
512 query_pairs.append_pair("sortBy", &sort_by.to_string());
513 }
514 if let Some(search) = &opts.search {
515 query_pairs.append_pair("search", search);
516 }
517 }
518 } let response = self
521 .parent
522 .http_client
523 .get(url)
524 .header("apikey", &self.parent.api_key)
525 .header("Authorization", format!("Bearer {}", &self.parent.api_key))
526 .send()
527 .await?;
528
529 if !response.status().is_success() {
530 let error_text = response.text().await?;
531 return Err(StorageError::ApiError(error_text));
532 }
533
534 let files = response.json::<Vec<FileObject>>().await?;
535
536 Ok(files)
537 }
538
539 pub async fn remove(&self, paths: Vec<&str>) -> Result<()> {
541 let url = format!(
542 "{}/storage/v1/object/{}",
543 self.parent.base_url, self.bucket_id
544 );
545
546 let payload = serde_json::json!({
547 "prefixes": paths
548 });
549
550 let response = self
551 .parent
552 .http_client
553 .delete(&url)
554 .header("apikey", &self.parent.api_key)
555 .header("Content-Type", "application/json")
556 .json(&payload)
557 .send()
558 .await?;
559
560 if !response.status().is_success() {
561 let error_text = response.text().await?;
562 return Err(StorageError::ApiError(error_text));
563 }
564
565 Ok(())
566 }
567
568 pub fn get_public_url(&self, path: &str) -> String {
570 format!(
571 "{}/storage/v1/object/public/{}/{}",
572 self.parent.base_url, self.bucket_id, path
573 )
574 }
575
576 pub async fn create_signed_url(&self, path: &str, expires_in: i32) -> Result<String> {
578 let url = format!(
579 "{}/storage/v1/object/sign/{}/{}",
580 self.parent.base_url, self.bucket_id, path
581 );
582
583 let payload = serde_json::json!({
584 "expiresIn": expires_in
585 });
586
587 let response = self
588 .parent
589 .http_client
590 .post(&url)
591 .header("apikey", &self.parent.api_key)
592 .header("Content-Type", "application/json")
593 .json(&payload)
594 .send()
595 .await?;
596
597 if !response.status().is_success() {
598 let error_text = response.text().await?;
599 return Err(StorageError::ApiError(error_text));
600 }
601
602 #[derive(Deserialize)]
603 struct SignedUrlResponse {
604 signed_url: String,
605 }
606
607 let signed_url = response.json::<SignedUrlResponse>().await?;
608
609 Ok(signed_url.signed_url)
610 }
611
612 pub async fn initiate_multipart_upload(
614 &self,
615 path: &str,
616 options: Option<FileOptions>,
617 ) -> Result<InitiateMultipartUploadResponse> {
618 let url = format!("{}/storage/v1/upload/initiate", self.parent.base_url);
619
620 let options = options.unwrap_or_default();
621
622 let cache_control = options
623 .cache_control
624 .unwrap_or_else(|| "max-age=3600".to_string());
625 let content_type = options
626 .content_type
627 .unwrap_or_else(|| "application/octet-stream".to_string());
628 let upsert = options.upsert.unwrap_or(false);
629
630 let payload = serde_json::json!({
631 "bucket": self.bucket_id,
632 "name": path,
633 "cacheControl": cache_control,
634 "contentType": content_type,
635 "upsert": upsert,
636 });
637
638 let response = self
639 .parent
640 .http_client
641 .post(&url)
642 .header("apikey", &self.parent.api_key)
643 .header("Content-Type", "application/json")
644 .json(&payload)
645 .send()
646 .await?;
647
648 if !response.status().is_success() {
649 let error_text = response.text().await?;
650 return Err(StorageError::ApiError(error_text));
651 }
652
653 let initiate_response: InitiateMultipartUploadResponse = response.json().await?;
654
655 Ok(initiate_response)
656 }
657
658 pub async fn upload_part(
660 &self,
661 upload_id: &str,
662 part_number: u32,
663 data: Bytes,
664 ) -> Result<UploadedPartInfo> {
665 let url = format!("{}/storage/v1/upload/part", self.parent.base_url);
666
667 let body = reqwest::Body::from(data);
668
669 let response = self
670 .parent
671 .http_client
672 .post(&url)
673 .header("apikey", &self.parent.api_key)
674 .query(&[
675 ("uploadId", upload_id),
676 ("partNumber", &part_number.to_string()),
677 ("bucket", &self.bucket_id),
678 ])
679 .body(body)
680 .send()
681 .await?;
682
683 if !response.status().is_success() {
684 let error_text = response.text().await?;
685 return Err(StorageError::ApiError(error_text));
686 }
687
688 let etag = response
689 .headers()
690 .get("etag")
691 .ok_or_else(|| StorageError::new("ETag header not found in response".to_string()))?
692 .to_str()
693 .map_err(|e| StorageError::new(format!("Invalid ETag header: {}", e)))?
694 .to_string();
695
696 let part_info = UploadedPartInfo { part_number, etag };
697
698 Ok(part_info)
699 }
700
701 pub async fn complete_multipart_upload(
703 &self,
704 upload_id: &str,
705 path: &str,
706 parts: Vec<UploadedPartInfo>,
707 ) -> Result<FileObject> {
708 let url = format!("{}/storage/v1/upload/complete", self.parent.base_url);
709
710 let payload = CompleteMultipartUploadRequest {
711 upload_id: upload_id.to_string(),
712 parts,
713 };
714
715 let response = self
716 .parent
717 .http_client
718 .post(&url)
719 .header("apikey", &self.parent.api_key)
720 .header("Content-Type", "application/json")
721 .query(&[("bucket", &self.bucket_id), ("key", &path.to_string())])
722 .json(&payload)
723 .send()
724 .await
725 .map_err(StorageError::NetworkError)?;
726
727 if !response.status().is_success() {
728 let error_text = response.text().await?;
729 return Err(StorageError::ApiError(error_text));
730 }
731
732 let file_object: FileObject = response.json().await?;
733
734 Ok(file_object)
735 }
736
737 pub async fn abort_multipart_upload(&self, upload_id: &str, path: &str) -> Result<()> {
739 let url = format!("{}/storage/v1/upload/abort", self.parent.base_url);
740
741 let payload = serde_json::json!({
742 "uploadId": upload_id,
743 "bucket": self.bucket_id,
744 "key": path,
745 });
746
747 let response = self
748 .parent
749 .http_client
750 .post(&url)
751 .header("apikey", &self.parent.api_key)
752 .header("Content-Type", "application/json")
753 .json(&payload)
754 .send()
755 .await?;
756
757 if !response.status().is_success() {
758 let error_text = response.text().await?;
759 return Err(StorageError::ApiError(error_text));
760 }
761
762 Ok(())
763 }
764
765 pub async fn upload_large_file(
771 &self,
772 path: &str,
773 file_path: &Path,
774 chunk_size: usize,
775 options: Option<FileOptions>,
776 ) -> Result<FileObject> {
777 let mut file = File::open(file_path).await?;
779
780 let file_size = file.metadata().await?.len() as usize;
782
783 let chunk_count = file_size.div_ceil(chunk_size);
785
786 if chunk_count == 0 {
787 return Err(StorageError::new("File is empty".to_string()));
788 }
789
790 let init_response = self.initiate_multipart_upload(path, options).await?;
792
793 let mut uploaded_parts = Vec::with_capacity(chunk_count);
795
796 let mut buffer = vec![0u8; chunk_size];
798
799 for part_number in 1..=chunk_count as u32 {
801 let n = file.read(&mut buffer).await?;
803
804 if n == 0 {
805 break;
806 }
807
808 let chunk_data = Bytes::from(buffer[0..n].to_vec());
810
811 let part_info = self
813 .upload_part(&init_response.upload_id, part_number, chunk_data)
814 .await?;
815
816 uploaded_parts.push(part_info);
818 }
819
820 let file_object = self
822 .complete_multipart_upload(&init_response.upload_id, path, uploaded_parts)
823 .await?;
824
825 Ok(file_object)
826 }
827
828 pub async fn transform_image(
830 &self,
831 path: &str,
832 options: ImageTransformOptions,
833 ) -> Result<Bytes> {
834 let url = format!(
835 "{}/object/transform/authenticated/{}/{}",
836 self.parent.base_url, self.bucket_id, path
837 );
838
839 let query_params = options.to_query_params();
841 let request_url = if query_params.is_empty() {
842 url
843 } else {
844 format!("{}?{}", url, query_params)
845 };
846
847 let res = self
848 .parent
849 .http_client
850 .get(&request_url)
851 .header("apikey", &self.parent.api_key)
852 .header("Authorization", format!("Bearer {}", self.parent.api_key))
853 .send()
854 .await
855 .map_err(StorageError::NetworkError)?;
856
857 let status = res.status();
859
860 if !status.is_success() {
861 let error_text = res
862 .text()
863 .await
864 .unwrap_or_else(|_| "Unknown error".to_string());
865 return Err(StorageError::ApiError(format!(
866 "Failed to transform image: {} (Status: {})",
867 error_text, status
868 )));
869 }
870
871 let bytes = res.bytes().await.map_err(StorageError::NetworkError)?;
872 Ok(bytes)
873 }
874
875 pub fn get_public_transform_url(&self, path: &str, options: ImageTransformOptions) -> String {
877 let base_url = format!(
878 "{}/object/public/{}/{}",
879 self.parent.base_url, self.bucket_id, path
880 );
881
882 let query_params = options.to_query_params();
884 if query_params.is_empty() {
885 base_url
886 } else {
887 format!("{}?{}", base_url, query_params)
888 }
889 }
890
891 pub async fn create_signed_transform_url(
893 &self,
894 path: &str,
895 options: ImageTransformOptions,
896 expires_in: i32,
897 ) -> Result<String> {
898 let url = format!(
899 "{}/object/sign/{}/{}",
900 self.parent.base_url, self.bucket_id, path
901 );
902
903 let transform_params = options.to_query_params();
905
906 let payload = json!({
907 "expiresIn": expires_in,
908 "transform": transform_params,
909 });
910
911 let res = self
912 .parent
913 .http_client
914 .post(&url)
915 .header("apikey", &self.parent.api_key)
916 .header("Authorization", format!("Bearer {}", self.parent.api_key))
917 .json(&payload)
918 .send()
919 .await
920 .map_err(StorageError::NetworkError)?;
921
922 let status = res.status();
924
925 if !status.is_success() {
926 let error_text = res
927 .text()
928 .await
929 .unwrap_or_else(|_| "Unknown error".to_string());
930 return Err(StorageError::ApiError(format!(
931 "Failed to create signed transform URL: {} (Status: {})",
932 error_text, status
933 )));
934 }
935
936 #[derive(Debug, Deserialize)]
937 struct SignedUrlResponse {
938 signed_url: String,
939 }
940
941 let response = res
942 .json::<SignedUrlResponse>()
943 .await
944 .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
945
946 Ok(response.signed_url)
947 }
948
949 pub fn s3_compatible(&self, options: s3::S3Options) -> s3::S3BucketClient {
951 s3::S3BucketClient::new(
952 &self.parent.base_url,
953 &self.parent.api_key,
954 &self.bucket_id,
955 self.parent.http_client.clone(),
956 options,
957 )
958 }
959}
960
961pub mod s3 {
963 use crate::{Result, StorageError};
964 use bytes::Bytes;
965 use reqwest::Client;
966 use serde::{Deserialize, Serialize};
967 use std::collections::HashMap;
968
969 #[derive(Debug, Clone, Serialize, Deserialize)]
971 pub struct S3Options {
972 pub access_key_id: String,
974 pub secret_access_key: String,
976 #[serde(skip_serializing_if = "Option::is_none")]
978 pub region: Option<String>,
979 #[serde(skip_serializing_if = "Option::is_none")]
981 pub endpoint: Option<String>,
982 #[serde(skip_serializing_if = "Option::is_none")]
984 pub force_path_style: Option<bool>,
985 }
986
987 impl Default for S3Options {
988 fn default() -> Self {
989 Self {
990 access_key_id: String::new(),
991 secret_access_key: String::new(),
992 region: Some("auto".to_string()),
993 endpoint: None,
994 force_path_style: Some(true),
995 }
996 }
997 }
998
999 pub struct S3Client {
1001 pub options: S3Options,
1002 pub base_url: String,
1003 pub api_key: String,
1004 pub http_client: Client,
1005 }
1006
1007 impl S3Client {
1008 pub fn new(base_url: &str, api_key: &str, http_client: Client, options: S3Options) -> Self {
1010 Self {
1011 options,
1012 base_url: base_url.to_string(),
1013 api_key: api_key.to_string(),
1014 http_client,
1015 }
1016 }
1017
1018 pub async fn create_bucket(&self, bucket_name: &str, is_public: bool) -> Result<()> {
1020 let url = format!("{}/storage/v1/bucket", self.base_url);
1021
1022 let payload = serde_json::json!({
1023 "name": bucket_name,
1024 "public": is_public,
1025 "file_size_limit": null,
1026 "allowed_mime_types": null
1027 });
1028
1029 let response = self
1030 .http_client
1031 .post(&url)
1032 .header("apikey", &self.api_key)
1033 .header("Authorization", format!("Bearer {}", &self.api_key))
1034 .json(&payload)
1035 .send()
1036 .await
1037 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1038
1039 if !response.status().is_success() {
1040 let error_text = response
1041 .text()
1042 .await
1043 .unwrap_or_else(|_| "Unknown error".to_string());
1044 return Err(StorageError::ApiError(error_text));
1045 }
1046
1047 Ok(())
1048 }
1049
1050 pub async fn delete_bucket(&self, bucket_name: &str) -> Result<()> {
1052 let url = format!("{}/storage/v1/bucket/{}", self.base_url, bucket_name);
1053
1054 let response = self
1055 .http_client
1056 .delete(&url)
1057 .header("apikey", &self.api_key)
1058 .header("Authorization", format!("Bearer {}", &self.api_key))
1059 .send()
1060 .await
1061 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1062
1063 if !response.status().is_success() {
1064 let error_text = response
1065 .text()
1066 .await
1067 .unwrap_or_else(|_| "Unknown error".to_string());
1068 return Err(StorageError::ApiError(error_text));
1069 }
1070
1071 Ok(())
1072 }
1073
1074 pub async fn list_buckets(&self) -> Result<Vec<serde_json::Value>> {
1076 let url = format!("{}/storage/v1/bucket", self.base_url);
1077
1078 let response = self
1079 .http_client
1080 .get(&url)
1081 .header("apikey", &self.api_key)
1082 .header("Authorization", format!("Bearer {}", &self.api_key))
1083 .send()
1084 .await
1085 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1086
1087 if !response.status().is_success() {
1088 let error_text = response
1089 .text()
1090 .await
1091 .unwrap_or_else(|_| "Unknown error".to_string());
1092 return Err(StorageError::ApiError(error_text));
1093 }
1094
1095 let buckets = response
1096 .json::<Vec<serde_json::Value>>()
1097 .await
1098 .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
1099
1100 Ok(buckets)
1101 }
1102
1103 pub fn bucket(&self, bucket_name: &str) -> S3BucketClient {
1105 S3BucketClient::new(
1106 &self.base_url,
1107 &self.api_key,
1108 bucket_name,
1109 self.http_client.clone(),
1110 self.options.clone(),
1111 )
1112 }
1113 }
1114
1115 pub struct S3BucketClient {
1117 pub base_url: String,
1118 pub api_key: String,
1119 pub bucket_name: String,
1120 pub http_client: Client,
1121 pub options: S3Options,
1122 }
1123
1124 impl S3BucketClient {
1125 pub fn new(
1127 base_url: &str,
1128 api_key: &str,
1129 bucket_name: &str,
1130 http_client: Client,
1131 options: S3Options,
1132 ) -> Self {
1133 Self {
1134 base_url: base_url.to_string(),
1135 api_key: api_key.to_string(),
1136 bucket_name: bucket_name.to_string(),
1137 http_client,
1138 options,
1139 }
1140 }
1141
1142 pub async fn put_object(
1144 &self,
1145 path: &str,
1146 data: Bytes,
1147 content_type: Option<String>,
1148 metadata: Option<HashMap<String, String>>,
1149 ) -> Result<()> {
1150 let url = format!(
1151 "{}/storage/v1/object/{}/{}",
1152 self.base_url,
1153 self.bucket_name,
1154 path.trim_start_matches('/')
1155 );
1156
1157 let content_type =
1158 content_type.unwrap_or_else(|| "application/octet-stream".to_string());
1159
1160 let mut request = self
1161 .http_client
1162 .put(&url)
1163 .header("apikey", &self.api_key)
1164 .header("Authorization", format!("Bearer {}", &self.api_key))
1165 .header("Content-Type", content_type)
1166 .body(data);
1167
1168 if let Some(metadata) = metadata {
1170 for (key, value) in metadata {
1171 request = request.header(&format!("x-amz-meta-{}", key), value);
1172 }
1173 }
1174
1175 let response = request
1176 .send()
1177 .await
1178 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1179
1180 if !response.status().is_success() {
1181 let error_text = response
1182 .text()
1183 .await
1184 .unwrap_or_else(|_| "Unknown error".to_string());
1185 return Err(StorageError::ApiError(error_text));
1186 }
1187
1188 Ok(())
1189 }
1190
1191 pub async fn get_object(&self, path: &str) -> Result<Bytes> {
1193 let url = format!(
1194 "{}/storage/v1/object/{}/{}",
1195 self.base_url,
1196 self.bucket_name,
1197 path.trim_start_matches('/')
1198 );
1199
1200 let response = self
1201 .http_client
1202 .get(&url)
1203 .header("apikey", &self.api_key)
1204 .header("Authorization", format!("Bearer {}", &self.api_key))
1205 .send()
1206 .await
1207 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1208
1209 if !response.status().is_success() {
1210 let error_text = response
1211 .text()
1212 .await
1213 .unwrap_or_else(|_| "Unknown error".to_string());
1214 return Err(StorageError::ApiError(error_text));
1215 }
1216
1217 let data = response
1218 .bytes()
1219 .await
1220 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1221
1222 Ok(data)
1223 }
1224
1225 pub async fn head_object(&self, path: &str) -> Result<HashMap<String, String>> {
1227 let url = format!(
1228 "{}/storage/v1/object/{}/{}",
1229 self.base_url,
1230 self.bucket_name,
1231 path.trim_start_matches('/')
1232 );
1233
1234 let response = self
1235 .http_client
1236 .head(&url)
1237 .header("apikey", &self.api_key)
1238 .header("Authorization", format!("Bearer {}", &self.api_key))
1239 .send()
1240 .await
1241 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1242
1243 if !response.status().is_success() {
1244 return Err(StorageError::ApiError("Object not found".to_string()));
1245 }
1246
1247 let mut metadata = HashMap::new();
1248
1249 for (key, value) in response.headers() {
1251 let key_str = key.to_string();
1252 if key_str.starts_with("x-amz-meta-") {
1253 let meta_key = key_str.trim_start_matches("x-amz-meta-").to_string();
1254 metadata.insert(meta_key, value.to_str().unwrap_or_default().to_string());
1255 }
1256 }
1257
1258 Ok(metadata)
1259 }
1260
1261 pub async fn delete_object(&self, path: &str) -> Result<()> {
1263 let url = format!(
1264 "{}/storage/v1/object/{}/{}",
1265 self.base_url,
1266 self.bucket_name,
1267 path.trim_start_matches('/')
1268 );
1269
1270 let response = self
1271 .http_client
1272 .delete(&url)
1273 .header("apikey", &self.api_key)
1274 .header("Authorization", format!("Bearer {}", &self.api_key))
1275 .send()
1276 .await
1277 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1278
1279 if !response.status().is_success() {
1280 let error_text = response
1281 .text()
1282 .await
1283 .unwrap_or_else(|_| "Unknown error".to_string());
1284 return Err(StorageError::ApiError(error_text));
1285 }
1286
1287 Ok(())
1288 }
1289
1290 pub async fn list_objects(
1292 &self,
1293 prefix: Option<&str>,
1294 delimiter: Option<&str>,
1295 max_keys: Option<i32>,
1296 ) -> Result<serde_json::Value> {
1297 let mut url = format!(
1298 "{}/storage/v1/object/list/{}",
1299 self.base_url, self.bucket_name
1300 );
1301
1302 let mut query_params = Vec::new();
1304
1305 if let Some(prefix) = prefix {
1306 query_params.push(format!("prefix={}", prefix));
1307 }
1308
1309 if let Some(delimiter) = delimiter {
1310 query_params.push(format!("delimiter={}", delimiter));
1311 }
1312
1313 if let Some(max_keys) = max_keys {
1314 query_params.push(format!("max-keys={}", max_keys));
1315 }
1316
1317 if !query_params.is_empty() {
1318 url = format!("{}?{}", url, query_params.join("&"));
1319 }
1320
1321 let response = self
1322 .http_client
1323 .get(&url)
1324 .header("apikey", &self.api_key)
1325 .header("Authorization", format!("Bearer {}", &self.api_key))
1326 .send()
1327 .await
1328 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1329
1330 if !response.status().is_success() {
1331 let error_text = response
1332 .text()
1333 .await
1334 .unwrap_or_else(|_| "Unknown error".to_string());
1335 return Err(StorageError::ApiError(error_text));
1336 }
1337
1338 let objects = response
1339 .json::<serde_json::Value>()
1340 .await
1341 .map_err(|e| StorageError::DeserializationError(e.to_string()))?;
1342
1343 Ok(objects)
1344 }
1345
1346 pub async fn copy_object(&self, source_path: &str, destination_path: &str) -> Result<()> {
1348 let url = format!("{}/storage/v1/object/copy", self.base_url);
1349
1350 let payload = serde_json::json!({
1351 "bucketId": self.bucket_name,
1352 "sourceKey": source_path,
1353 "destinationKey": destination_path
1354 });
1355
1356 let response = self
1357 .http_client
1358 .post(&url)
1359 .header("apikey", &self.api_key)
1360 .header("Authorization", format!("Bearer {}", &self.api_key))
1361 .json(&payload)
1362 .send()
1363 .await
1364 .map_err(|e| StorageError::RequestError(e.to_string()))?;
1365
1366 if !response.status().is_success() {
1367 let error_text = response
1368 .text()
1369 .await
1370 .unwrap_or_else(|_| "Unknown error".to_string());
1371 return Err(StorageError::ApiError(error_text));
1372 }
1373
1374 Ok(())
1375 }
1376 }
1377}
1378
1379#[cfg(test)]
1380mod tests {
1381 use super::*;
1382 use serde_json::json;
1383 use wiremock::matchers::{method, path, query_param};
1384 use wiremock::{Mock, MockServer, ResponseTemplate};
1385
1386 #[tokio::test]
1387 async fn test_list_buckets() {
1388 }
1390
1391 #[tokio::test]
1392 async fn test_multipart_upload() {
1393 }
1396
1397 #[tokio::test]
1398 async fn test_transform_image() {
1399 }
1402
1403 #[tokio::test]
1404 async fn test_get_public_transform_url() {
1405 let http_client = reqwest::Client::new();
1406 let storage_client = StorageClient::new("https://example.com", "fake-key", http_client);
1407 let bucket_client = storage_client.from("test-bucket");
1408
1409 let options = ImageTransformOptions::new()
1410 .with_width(300)
1411 .with_height(200)
1412 .with_format("webp");
1413
1414 let url = bucket_client.get_public_transform_url("image.jpg", options);
1415
1416 assert!(url.contains("https://example.com"));
1418 assert!(url.contains("test-bucket"));
1419 assert!(url.contains("image.jpg"));
1420 assert!(url.contains("width=300"));
1422 assert!(url.contains("height=200"));
1423 assert!(url.contains("format=webp"));
1424 }
1425
1426 #[tokio::test]
1427 async fn test_create_signed_transform_url() {
1428 }
1431}