1use 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#[derive(Debug, Clone)]
21pub struct Storage {
22 http_client: Arc<HttpClient>,
23 config: Arc<SupabaseConfig>,
24}
25
26#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "lowercase")]
80pub enum ResizeMode {
81 Cover,
82 Contain,
83 Fill,
84}
85
86#[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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 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 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 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 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 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 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 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}