1use 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#[derive(Debug, Clone)]
16pub struct Storage {
17 http_client: Arc<HttpClient>,
18 config: Arc<SupabaseConfig>,
19}
20
21#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum ResizeMode {
76 Cover,
77 Contain,
78 Fill,
79}
80
81#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}