supabase/
storage.rs

1//! Storage module for Supabase file operations
2
3use crate::{
4    error::{Error, Result},
5    types::{SupabaseConfig, Timestamp},
6};
7use bytes::Bytes;
8
9#[cfg(target_arch = "wasm32")]
10use reqwest::Client as HttpClient;
11#[cfg(not(target_arch = "wasm32"))]
12use reqwest::{multipart, Client as HttpClient};
13use serde::{Deserialize, Serialize};
14use std::{collections::HashMap, sync::Arc};
15
16use tracing::{debug, info};
17use url::Url;
18
19/// Storage client for file operations
20#[derive(Debug, Clone)]
21pub struct Storage {
22    http_client: Arc<HttpClient>,
23    config: Arc<SupabaseConfig>,
24}
25
26/// Storage bucket information
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Bucket {
29    pub id: String,
30    pub name: String,
31    pub owner: Option<String>,
32    pub public: bool,
33    pub file_size_limit: Option<u64>,
34    pub allowed_mime_types: Option<Vec<String>>,
35    pub created_at: Timestamp,
36    pub updated_at: Timestamp,
37}
38
39/// File object information
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct FileObject {
42    pub name: String,
43    pub id: Option<String>,
44    pub updated_at: Option<Timestamp>,
45    pub created_at: Option<Timestamp>,
46    pub last_accessed_at: Option<Timestamp>,
47    pub metadata: Option<HashMap<String, serde_json::Value>>,
48}
49
50/// Upload response
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct UploadResponse {
53    #[serde(rename = "Key")]
54    pub key: String,
55    #[serde(rename = "Id")]
56    pub id: Option<String>,
57}
58
59/// File options for upload
60#[derive(Debug, Clone, Default)]
61pub struct FileOptions {
62    pub cache_control: Option<String>,
63    pub content_type: Option<String>,
64    pub upsert: bool,
65}
66
67/// Transform options for image processing
68#[derive(Debug, Clone)]
69pub struct TransformOptions {
70    pub width: Option<u32>,
71    pub height: Option<u32>,
72    pub resize: Option<ResizeMode>,
73    pub format: Option<ImageFormat>,
74    pub quality: Option<u8>,
75}
76
77/// Resize mode for image transformations
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "lowercase")]
80pub enum ResizeMode {
81    Cover,
82    Contain,
83    Fill,
84}
85
86/// Image format for transformations
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "lowercase")]
89pub enum ImageFormat {
90    Webp,
91    Jpeg,
92    Png,
93    Avif,
94}
95
96impl Storage {
97    /// Create a new Storage instance
98    pub fn new(config: Arc<SupabaseConfig>, http_client: Arc<HttpClient>) -> Result<Self> {
99        debug!("Initializing Storage module");
100
101        Ok(Self {
102            http_client,
103            config,
104        })
105    }
106
107    /// Get the appropriate authorization key for admin operations
108    fn get_admin_key(&self) -> &str {
109        self.config
110            .service_role_key
111            .as_ref()
112            .unwrap_or(&self.config.key)
113    }
114
115    /// List all storage buckets
116    pub async fn list_buckets(&self) -> Result<Vec<Bucket>> {
117        debug!("Listing all storage buckets");
118
119        let url = format!("{}/storage/v1/bucket", self.config.url);
120        let response = self.http_client.get(&url).send().await?;
121
122        if !response.status().is_success() {
123            let status = response.status();
124            let error_msg = match response.text().await {
125                Ok(text) => text,
126                Err(_) => format!("List buckets failed with status: {}", status),
127            };
128            return Err(Error::storage(error_msg));
129        }
130
131        let buckets: Vec<Bucket> = response.json().await?;
132        info!("Listed {} buckets successfully", buckets.len());
133
134        Ok(buckets)
135    }
136
137    /// Get bucket information
138    pub async fn get_bucket(&self, bucket_id: &str) -> Result<Bucket> {
139        debug!("Getting bucket info for: {}", bucket_id);
140
141        let url = format!("{}/storage/v1/bucket/{}", self.config.url, bucket_id);
142        let response = self.http_client.get(&url).send().await?;
143
144        if !response.status().is_success() {
145            let status = response.status();
146            let error_msg = match response.text().await {
147                Ok(text) => text,
148                Err(_) => format!("Get bucket failed with status: {}", status),
149            };
150            return Err(Error::storage(error_msg));
151        }
152
153        let bucket: Bucket = response.json().await?;
154        info!("Retrieved bucket info for: {}", bucket_id);
155
156        Ok(bucket)
157    }
158
159    /// Create a new storage bucket
160    pub async fn create_bucket(&self, id: &str, name: &str, public: bool) -> Result<Bucket> {
161        debug!("Creating bucket: {} ({})", name, id);
162
163        let payload = serde_json::json!({
164            "id": id,
165            "name": name,
166            "public": public
167        });
168
169        let url = format!("{}/storage/v1/bucket", self.config.url);
170        let response = self
171            .http_client
172            .post(&url)
173            .header("Authorization", format!("Bearer {}", self.get_admin_key()))
174            .json(&payload)
175            .send()
176            .await?;
177
178        if !response.status().is_success() {
179            let status = response.status();
180            let error_msg = match response.text().await {
181                Ok(text) => text,
182                Err(_) => format!("Create bucket failed with status: {}", status),
183            };
184            return Err(Error::storage(error_msg));
185        }
186
187        let bucket: Bucket = response.json().await?;
188        info!("Created bucket successfully: {}", id);
189
190        Ok(bucket)
191    }
192
193    /// Update bucket settings
194    pub async fn update_bucket(&self, id: &str, public: Option<bool>) -> Result<()> {
195        debug!("Updating bucket: {}", id);
196
197        let mut payload = serde_json::Map::new();
198        if let Some(public) = public {
199            payload.insert("public".to_string(), serde_json::Value::Bool(public));
200        }
201
202        let url = format!("{}/storage/v1/bucket/{}", self.config.url, id);
203        let response = self
204            .http_client
205            .put(&url)
206            .header("Authorization", format!("Bearer {}", self.get_admin_key()))
207            .json(&payload)
208            .send()
209            .await?;
210
211        if !response.status().is_success() {
212            let status = response.status();
213            let error_msg = match response.text().await {
214                Ok(text) => text,
215                Err(_) => format!("Update bucket failed with status: {}", status),
216            };
217            return Err(Error::storage(error_msg));
218        }
219
220        info!("Updated bucket successfully: {}", id);
221        Ok(())
222    }
223
224    /// Delete a storage bucket
225    pub async fn delete_bucket(&self, id: &str) -> Result<()> {
226        debug!("Deleting bucket: {}", id);
227
228        let url = format!("{}/storage/v1/bucket/{}", self.config.url, id);
229        let response = self
230            .http_client
231            .delete(&url)
232            .header("Authorization", format!("Bearer {}", self.get_admin_key()))
233            .send()
234            .await?;
235
236        if !response.status().is_success() {
237            let status = response.status();
238            let error_msg = match response.text().await {
239                Ok(text) => text,
240                Err(_) => format!("Delete bucket failed with status: {}", status),
241            };
242            return Err(Error::storage(error_msg));
243        }
244
245        info!("Deleted bucket successfully: {}", id);
246        Ok(())
247    }
248
249    /// List files in a bucket
250    pub async fn list(&self, bucket_id: &str, path: Option<&str>) -> Result<Vec<FileObject>> {
251        debug!("Listing files in bucket: {}", bucket_id);
252
253        let url = format!("{}/storage/v1/object/list/{}", self.config.url, bucket_id);
254
255        let payload = serde_json::json!({
256            "prefix": path.unwrap_or("")
257        });
258
259        let response = self.http_client.post(&url).json(&payload).send().await?;
260
261        if !response.status().is_success() {
262            let status = response.status();
263            let error_msg = match response.text().await {
264                Ok(text) => text,
265                Err(_) => format!("List files failed with status: {}", status),
266            };
267            return Err(Error::storage(error_msg));
268        }
269
270        let files: Vec<FileObject> = response.json().await?;
271        info!("Listed {} files in bucket: {}", files.len(), bucket_id);
272
273        Ok(files)
274    }
275
276    /// Upload a file from bytes
277    #[cfg(not(target_arch = "wasm32"))]
278    pub async fn upload(
279        &self,
280        bucket_id: &str,
281        path: &str,
282        file_body: Bytes,
283        options: Option<FileOptions>,
284    ) -> Result<UploadResponse> {
285        debug!("Uploading file to bucket: {} at path: {}", bucket_id, path);
286
287        let options = options.unwrap_or_default();
288
289        let url = format!(
290            "{}/storage/v1/object/{}/{}",
291            self.config.url, bucket_id, path
292        );
293
294        let mut form = multipart::Form::new().part(
295            "file",
296            multipart::Part::bytes(file_body.to_vec()).file_name(path.to_string()),
297        );
298
299        if let Some(content_type) = options.content_type {
300            form = form.part("contentType", multipart::Part::text(content_type));
301        }
302
303        if let Some(cache_control) = options.cache_control {
304            form = form.part("cacheControl", multipart::Part::text(cache_control));
305        }
306
307        let mut request = self.http_client.post(&url).multipart(form);
308
309        if options.upsert {
310            request = request.header("x-upsert", "true");
311        }
312
313        let response = request.send().await?;
314
315        if !response.status().is_success() {
316            let status = response.status();
317            let error_msg = match response.text().await {
318                Ok(text) => text,
319                Err(_) => format!("Upload failed with status: {}", status),
320            };
321            return Err(Error::storage(error_msg));
322        }
323
324        let upload_response: UploadResponse = response.json().await?;
325        info!("Uploaded file successfully: {}", path);
326        Ok(upload_response)
327    }
328
329    /// Upload a file from bytes (WASM version)
330    ///
331    /// Note: WASM version uses simpler body upload due to multipart limitations
332    #[cfg(target_arch = "wasm32")]
333    pub async fn upload(
334        &self,
335        bucket_id: &str,
336        path: &str,
337        file_body: Bytes,
338        options: Option<FileOptions>,
339    ) -> Result<UploadResponse> {
340        debug!(
341            "Uploading file to bucket: {} at path: {} (WASM)",
342            bucket_id, path
343        );
344
345        let options = options.unwrap_or_default();
346
347        let url = format!(
348            "{}/storage/v1/object/{}/{}",
349            self.config.url, bucket_id, path
350        );
351
352        let mut request = self.http_client.post(&url).body(file_body);
353
354        if let Some(content_type) = options.content_type {
355            request = request.header("Content-Type", content_type);
356        }
357
358        if let Some(cache_control) = options.cache_control {
359            request = request.header("Cache-Control", cache_control);
360        }
361
362        if options.upsert {
363            request = request.header("x-upsert", "true");
364        }
365
366        let response = request.send().await?;
367
368        if !response.status().is_success() {
369            let status = response.status();
370            let error_msg = match response.text().await {
371                Ok(text) => text,
372                Err(_) => format!("Upload failed with status: {}", status),
373            };
374            return Err(Error::storage(error_msg));
375        }
376
377        let upload_response: UploadResponse = response.json().await?;
378        info!("Uploaded file successfully: {}", path);
379
380        Ok(upload_response)
381    }
382
383    /// Upload a file from local filesystem (Native only, requires tokio)
384    #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
385    pub async fn upload_file<P: AsRef<std::path::Path>>(
386        &self,
387        bucket_id: &str,
388        path: &str,
389        file_path: P,
390        options: Option<FileOptions>,
391    ) -> Result<UploadResponse> {
392        debug!("Uploading file from path: {:?}", file_path.as_ref());
393
394        let file_bytes = tokio::fs::read(file_path)
395            .await
396            .map_err(|e| Error::storage(format!("Failed to read file: {}", e)))?;
397
398        self.upload(bucket_id, path, Bytes::from(file_bytes), options)
399            .await
400    }
401
402    /// Download a file
403    pub async fn download(&self, bucket_id: &str, path: &str) -> Result<Bytes> {
404        debug!(
405            "Downloading file from bucket: {} at path: {}",
406            bucket_id, path
407        );
408
409        let url = format!(
410            "{}/storage/v1/object/{}/{}",
411            self.config.url, bucket_id, path
412        );
413
414        let response = self.http_client.get(&url).send().await?;
415
416        if !response.status().is_success() {
417            let error_msg = format!("Download failed with status: {}", response.status());
418            return Err(Error::storage(error_msg));
419        }
420
421        let bytes = response.bytes().await?;
422        info!("Downloaded file successfully: {}", path);
423
424        Ok(bytes)
425    }
426
427    /// Delete a file
428    pub async fn remove(&self, bucket_id: &str, paths: &[&str]) -> Result<()> {
429        debug!("Deleting files from bucket: {}", bucket_id);
430
431        let url = format!("{}/storage/v1/object/{}", self.config.url, bucket_id);
432
433        let payload = serde_json::json!({
434            "prefixes": paths
435        });
436
437        let response = self.http_client.delete(&url).json(&payload).send().await?;
438
439        if !response.status().is_success() {
440            let status = response.status();
441            let error_msg = match response.text().await {
442                Ok(text) => text,
443                Err(_) => format!("Delete failed with status: {}", status),
444            };
445            return Err(Error::storage(error_msg));
446        }
447
448        info!("Deleted {} files successfully", paths.len());
449        Ok(())
450    }
451
452    /// Move a file
453    pub async fn r#move(&self, bucket_id: &str, from_path: &str, to_path: &str) -> Result<()> {
454        debug!("Moving file from {} to {}", from_path, to_path);
455
456        let url = format!("{}/storage/v1/object/move", self.config.url);
457
458        let payload = serde_json::json!({
459            "bucketId": bucket_id,
460            "sourceKey": from_path,
461            "destinationKey": to_path
462        });
463
464        let response = self.http_client.post(&url).json(&payload).send().await?;
465
466        if !response.status().is_success() {
467            let status = response.status();
468            let error_msg = match response.text().await {
469                Ok(text) => text,
470                Err(_) => format!("Move failed with status: {}", status),
471            };
472            return Err(Error::storage(error_msg));
473        }
474
475        info!("Moved file successfully from {} to {}", from_path, to_path);
476        Ok(())
477    }
478
479    /// Copy a file
480    pub async fn copy(&self, bucket_id: &str, from_path: &str, to_path: &str) -> Result<()> {
481        debug!("Copying file from {} to {}", from_path, to_path);
482
483        let url = format!("{}/storage/v1/object/copy", self.config.url);
484
485        let payload = serde_json::json!({
486            "bucketId": bucket_id,
487            "sourceKey": from_path,
488            "destinationKey": to_path
489        });
490
491        let response = self.http_client.post(&url).json(&payload).send().await?;
492
493        if !response.status().is_success() {
494            let status = response.status();
495            let error_msg = match response.text().await {
496                Ok(text) => text,
497                Err(_) => format!("Copy failed with status: {}", status),
498            };
499            return Err(Error::storage(error_msg));
500        }
501
502        info!("Copied file successfully from {} to {}", from_path, to_path);
503        Ok(())
504    }
505
506    /// Get public URL for a file
507    pub fn get_public_url(&self, bucket_id: &str, path: &str) -> String {
508        format!(
509            "{}/storage/v1/object/public/{}/{}",
510            self.config.url, bucket_id, path
511        )
512    }
513
514    /// Create a signed URL for private file access
515    pub async fn create_signed_url(
516        &self,
517        bucket_id: &str,
518        path: &str,
519        expires_in_seconds: u64,
520    ) -> Result<String> {
521        debug!("Creating signed URL for file: {}", path);
522
523        let url = format!("{}/storage/v1/object/sign/{}", self.config.url, bucket_id);
524
525        let payload = serde_json::json!({
526            "expiresIn": expires_in_seconds,
527            "paths": [path]
528        });
529
530        let response = self.http_client.post(&url).json(&payload).send().await?;
531
532        if !response.status().is_success() {
533            let status = response.status();
534            let error_msg = match response.text().await {
535                Ok(text) => text,
536                Err(_) => format!("Create signed URL failed with status: {}", status),
537            };
538            return Err(Error::storage(error_msg));
539        }
540
541        let result: serde_json::Value = response.json().await?;
542        let signed_urls = result["signedUrls"].as_array().ok_or_else(|| {
543            Error::storage("Invalid signed URL response - missing signedUrls array")
544        })?;
545
546        let first = signed_urls.first().ok_or_else(|| {
547            Error::storage("Invalid signed URL response - empty signedUrls array")
548        })?;
549
550        let first_url = first["signedUrl"].as_str().ok_or_else(|| {
551            Error::storage("Invalid signed URL response - missing signedUrl field")
552        })?;
553
554        info!("Created signed URL successfully for: {}", path);
555        Ok(first_url.to_string())
556    }
557
558    /// Get transformed image URL
559    pub fn get_public_url_transformed(
560        &self,
561        bucket_id: &str,
562        path: &str,
563        options: TransformOptions,
564    ) -> Result<String> {
565        let mut url = Url::parse(&self.get_public_url(bucket_id, path))?;
566
567        if let Some(width) = options.width {
568            url.query_pairs_mut()
569                .append_pair("width", &width.to_string());
570        }
571
572        if let Some(height) = options.height {
573            url.query_pairs_mut()
574                .append_pair("height", &height.to_string());
575        }
576
577        if let Some(resize) = options.resize {
578            let resize_str = match resize {
579                ResizeMode::Cover => "cover",
580                ResizeMode::Contain => "contain",
581                ResizeMode::Fill => "fill",
582            };
583            url.query_pairs_mut().append_pair("resize", resize_str);
584        }
585
586        if let Some(format) = options.format {
587            let format_str = match format {
588                ImageFormat::Webp => "webp",
589                ImageFormat::Jpeg => "jpeg",
590                ImageFormat::Png => "png",
591                ImageFormat::Avif => "avif",
592            };
593            url.query_pairs_mut().append_pair("format", format_str);
594        }
595
596        if let Some(quality) = options.quality {
597            url.query_pairs_mut()
598                .append_pair("quality", &quality.to_string());
599        }
600
601        Ok(url.to_string())
602    }
603}