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