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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}