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    /// Get the appropriate authorization key for admin operations
103    fn get_admin_key(&self) -> &str {
104        self.config
105            .service_role_key
106            .as_ref()
107            .unwrap_or(&self.config.key)
108    }
109
110    /// List all storage buckets
111    pub async fn list_buckets(&self) -> Result<Vec<Bucket>> {
112        debug!("Listing all storage buckets");
113
114        let url = format!("{}/storage/v1/bucket", self.config.url);
115        let response = self.http_client.get(&url).send().await?;
116
117        if !response.status().is_success() {
118            let status = response.status();
119            let error_msg = match response.text().await {
120                Ok(text) => text,
121                Err(_) => format!("List buckets failed with status: {}", status),
122            };
123            return Err(Error::storage(error_msg));
124        }
125
126        let buckets: Vec<Bucket> = response.json().await?;
127        info!("Listed {} buckets successfully", buckets.len());
128
129        Ok(buckets)
130    }
131
132    /// Get bucket information
133    pub async fn get_bucket(&self, bucket_id: &str) -> Result<Bucket> {
134        debug!("Getting bucket info for: {}", bucket_id);
135
136        let url = format!("{}/storage/v1/bucket/{}", self.config.url, bucket_id);
137        let response = self.http_client.get(&url).send().await?;
138
139        if !response.status().is_success() {
140            let status = response.status();
141            let error_msg = match response.text().await {
142                Ok(text) => text,
143                Err(_) => format!("Get bucket failed with status: {}", status),
144            };
145            return Err(Error::storage(error_msg));
146        }
147
148        let bucket: Bucket = response.json().await?;
149        info!("Retrieved bucket info for: {}", bucket_id);
150
151        Ok(bucket)
152    }
153
154    /// Create a new storage bucket
155    pub async fn create_bucket(&self, id: &str, name: &str, public: bool) -> Result<Bucket> {
156        debug!("Creating bucket: {} ({})", name, id);
157
158        let payload = serde_json::json!({
159            "id": id,
160            "name": name,
161            "public": public
162        });
163
164        let url = format!("{}/storage/v1/bucket", self.config.url);
165        let response = self
166            .http_client
167            .post(&url)
168            .header("Authorization", format!("Bearer {}", self.get_admin_key()))
169            .json(&payload)
170            .send()
171            .await?;
172
173        if !response.status().is_success() {
174            let status = response.status();
175            let error_msg = match response.text().await {
176                Ok(text) => text,
177                Err(_) => format!("Create bucket failed with status: {}", status),
178            };
179            return Err(Error::storage(error_msg));
180        }
181
182        let bucket: Bucket = response.json().await?;
183        info!("Created bucket successfully: {}", id);
184
185        Ok(bucket)
186    }
187
188    /// Update bucket settings
189    pub async fn update_bucket(&self, id: &str, public: Option<bool>) -> Result<()> {
190        debug!("Updating bucket: {}", id);
191
192        let mut payload = serde_json::Map::new();
193        if let Some(public) = public {
194            payload.insert("public".to_string(), serde_json::Value::Bool(public));
195        }
196
197        let url = format!("{}/storage/v1/bucket/{}", self.config.url, id);
198        let response = self
199            .http_client
200            .put(&url)
201            .header("Authorization", format!("Bearer {}", self.get_admin_key()))
202            .json(&payload)
203            .send()
204            .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!("Update bucket failed with status: {}", status),
211            };
212            return Err(Error::storage(error_msg));
213        }
214
215        info!("Updated bucket successfully: {}", id);
216        Ok(())
217    }
218
219    /// Delete a storage bucket
220    pub async fn delete_bucket(&self, id: &str) -> Result<()> {
221        debug!("Deleting bucket: {}", id);
222
223        let url = format!("{}/storage/v1/bucket/{}", self.config.url, id);
224        let response = self
225            .http_client
226            .delete(&url)
227            .header("Authorization", format!("Bearer {}", self.get_admin_key()))
228            .send()
229            .await?;
230
231        if !response.status().is_success() {
232            let status = response.status();
233            let error_msg = match response.text().await {
234                Ok(text) => text,
235                Err(_) => format!("Delete bucket failed with status: {}", status),
236            };
237            return Err(Error::storage(error_msg));
238        }
239
240        info!("Deleted bucket successfully: {}", id);
241        Ok(())
242    }
243
244    /// List files in a bucket
245    pub async fn list(&self, bucket_id: &str, path: Option<&str>) -> Result<Vec<FileObject>> {
246        debug!("Listing files in bucket: {}", bucket_id);
247
248        let url = format!("{}/storage/v1/object/list/{}", self.config.url, bucket_id);
249
250        let payload = serde_json::json!({
251            "prefix": path.unwrap_or("")
252        });
253
254        let response = self.http_client.post(&url).json(&payload).send().await?;
255
256        if !response.status().is_success() {
257            let status = response.status();
258            let error_msg = match response.text().await {
259                Ok(text) => text,
260                Err(_) => format!("List files failed with status: {}", status),
261            };
262            return Err(Error::storage(error_msg));
263        }
264
265        let files: Vec<FileObject> = response.json().await?;
266        info!("Listed {} files in bucket: {}", files.len(), bucket_id);
267
268        Ok(files)
269    }
270
271    /// Upload a file from bytes
272    pub async fn upload(
273        &self,
274        bucket_id: &str,
275        path: &str,
276        file_body: Bytes,
277        options: Option<FileOptions>,
278    ) -> Result<UploadResponse> {
279        debug!("Uploading file to bucket: {} at path: {}", bucket_id, path);
280
281        let options = options.unwrap_or_default();
282
283        let url = format!(
284            "{}/storage/v1/object/{}/{}",
285            self.config.url, bucket_id, path
286        );
287
288        let mut form = multipart::Form::new().part(
289            "file",
290            multipart::Part::bytes(file_body.to_vec()).file_name(path.to_string()),
291        );
292
293        if let Some(content_type) = options.content_type {
294            form = form.part("contentType", multipart::Part::text(content_type));
295        }
296
297        if let Some(cache_control) = options.cache_control {
298            form = form.part("cacheControl", multipart::Part::text(cache_control));
299        }
300
301        let mut request = self.http_client.post(&url).multipart(form);
302
303        if options.upsert {
304            request = request.header("x-upsert", "true");
305        }
306
307        let response = request.send().await?;
308
309        if !response.status().is_success() {
310            let status = response.status();
311            let error_msg = match response.text().await {
312                Ok(text) => text,
313                Err(_) => format!("Upload failed with status: {}", status),
314            };
315            return Err(Error::storage(error_msg));
316        }
317
318        let upload_response: UploadResponse = response.json().await?;
319        info!("Uploaded file successfully: {}", path);
320
321        Ok(upload_response)
322    }
323
324    /// Upload a file from local filesystem
325    pub async fn upload_file<P: AsRef<Path>>(
326        &self,
327        bucket_id: &str,
328        path: &str,
329        file_path: P,
330        options: Option<FileOptions>,
331    ) -> Result<UploadResponse> {
332        debug!("Uploading file from path: {:?}", file_path.as_ref());
333
334        let file_bytes = tokio::fs::read(file_path)
335            .await
336            .map_err(|e| Error::storage(format!("Failed to read file: {}", e)))?;
337
338        self.upload(bucket_id, path, Bytes::from(file_bytes), options)
339            .await
340    }
341
342    /// Download a file
343    pub async fn download(&self, bucket_id: &str, path: &str) -> Result<Bytes> {
344        debug!(
345            "Downloading file from bucket: {} at path: {}",
346            bucket_id, path
347        );
348
349        let url = format!(
350            "{}/storage/v1/object/{}/{}",
351            self.config.url, bucket_id, path
352        );
353
354        let response = self.http_client.get(&url).send().await?;
355
356        if !response.status().is_success() {
357            let error_msg = format!("Download failed with status: {}", response.status());
358            return Err(Error::storage(error_msg));
359        }
360
361        let bytes = response.bytes().await?;
362        info!("Downloaded file successfully: {}", path);
363
364        Ok(bytes)
365    }
366
367    /// Delete a file
368    pub async fn remove(&self, bucket_id: &str, paths: &[&str]) -> Result<()> {
369        debug!("Deleting files from bucket: {}", bucket_id);
370
371        let url = format!("{}/storage/v1/object/{}", self.config.url, bucket_id);
372
373        let payload = serde_json::json!({
374            "prefixes": paths
375        });
376
377        let response = self.http_client.delete(&url).json(&payload).send().await?;
378
379        if !response.status().is_success() {
380            let status = response.status();
381            let error_msg = match response.text().await {
382                Ok(text) => text,
383                Err(_) => format!("Delete failed with status: {}", status),
384            };
385            return Err(Error::storage(error_msg));
386        }
387
388        info!("Deleted {} files successfully", paths.len());
389        Ok(())
390    }
391
392    /// Move a file
393    pub async fn r#move(&self, bucket_id: &str, from_path: &str, to_path: &str) -> Result<()> {
394        debug!("Moving file from {} to {}", from_path, to_path);
395
396        let url = format!("{}/storage/v1/object/move", self.config.url);
397
398        let payload = serde_json::json!({
399            "bucketId": bucket_id,
400            "sourceKey": from_path,
401            "destinationKey": to_path
402        });
403
404        let response = self.http_client.post(&url).json(&payload).send().await?;
405
406        if !response.status().is_success() {
407            let status = response.status();
408            let error_msg = match response.text().await {
409                Ok(text) => text,
410                Err(_) => format!("Move failed with status: {}", status),
411            };
412            return Err(Error::storage(error_msg));
413        }
414
415        info!("Moved file successfully from {} to {}", from_path, to_path);
416        Ok(())
417    }
418
419    /// Copy a file
420    pub async fn copy(&self, bucket_id: &str, from_path: &str, to_path: &str) -> Result<()> {
421        debug!("Copying file from {} to {}", from_path, to_path);
422
423        let url = format!("{}/storage/v1/object/copy", self.config.url);
424
425        let payload = serde_json::json!({
426            "bucketId": bucket_id,
427            "sourceKey": from_path,
428            "destinationKey": to_path
429        });
430
431        let response = self.http_client.post(&url).json(&payload).send().await?;
432
433        if !response.status().is_success() {
434            let status = response.status();
435            let error_msg = match response.text().await {
436                Ok(text) => text,
437                Err(_) => format!("Copy failed with status: {}", status),
438            };
439            return Err(Error::storage(error_msg));
440        }
441
442        info!("Copied file successfully from {} to {}", from_path, to_path);
443        Ok(())
444    }
445
446    /// Get public URL for a file
447    pub fn get_public_url(&self, bucket_id: &str, path: &str) -> String {
448        format!(
449            "{}/storage/v1/object/public/{}/{}",
450            self.config.url, bucket_id, path
451        )
452    }
453
454    /// Create a signed URL for private file access
455    pub async fn create_signed_url(
456        &self,
457        bucket_id: &str,
458        path: &str,
459        expires_in_seconds: u64,
460    ) -> Result<String> {
461        debug!("Creating signed URL for file: {}", path);
462
463        let url = format!("{}/storage/v1/object/sign/{}", self.config.url, bucket_id);
464
465        let payload = serde_json::json!({
466            "expiresIn": expires_in_seconds,
467            "paths": [path]
468        });
469
470        let response = self.http_client.post(&url).json(&payload).send().await?;
471
472        if !response.status().is_success() {
473            let status = response.status();
474            let error_msg = match response.text().await {
475                Ok(text) => text,
476                Err(_) => format!("Create signed URL failed with status: {}", status),
477            };
478            return Err(Error::storage(error_msg));
479        }
480
481        let result: serde_json::Value = response.json().await?;
482        let signed_urls = result["signedUrls"].as_array().ok_or_else(|| {
483            Error::storage("Invalid signed URL response - missing signedUrls array")
484        })?;
485
486        let first = signed_urls.first().ok_or_else(|| {
487            Error::storage("Invalid signed URL response - empty signedUrls array")
488        })?;
489
490        let first_url = first["signedUrl"].as_str().ok_or_else(|| {
491            Error::storage("Invalid signed URL response - missing signedUrl field")
492        })?;
493
494        info!("Created signed URL successfully for: {}", path);
495        Ok(first_url.to_string())
496    }
497
498    /// Get transformed image URL
499    pub fn get_public_url_transformed(
500        &self,
501        bucket_id: &str,
502        path: &str,
503        options: TransformOptions,
504    ) -> Result<String> {
505        let mut url = Url::parse(&self.get_public_url(bucket_id, path))?;
506
507        if let Some(width) = options.width {
508            url.query_pairs_mut()
509                .append_pair("width", &width.to_string());
510        }
511
512        if let Some(height) = options.height {
513            url.query_pairs_mut()
514                .append_pair("height", &height.to_string());
515        }
516
517        if let Some(resize) = options.resize {
518            let resize_str = match resize {
519                ResizeMode::Cover => "cover",
520                ResizeMode::Contain => "contain",
521                ResizeMode::Fill => "fill",
522            };
523            url.query_pairs_mut().append_pair("resize", resize_str);
524        }
525
526        if let Some(format) = options.format {
527            let format_str = match format {
528                ImageFormat::Webp => "webp",
529                ImageFormat::Jpeg => "jpeg",
530                ImageFormat::Png => "png",
531                ImageFormat::Avif => "avif",
532            };
533            url.query_pairs_mut().append_pair("format", format_str);
534        }
535
536        if let Some(quality) = options.quality {
537            url.query_pairs_mut()
538                .append_pair("quality", &quality.to_string());
539        }
540
541        Ok(url.to_string())
542    }
543}