use crate::core::SupabaseClient;
use crate::error::{Result, SupaError};
use reqwest::multipart;
use serde::Deserialize;
use std::path::Path;
#[derive(Clone)]
pub struct StorageClient {
client: SupabaseClient,
}
impl StorageClient {
pub(crate) fn new(client: SupabaseClient) -> Self {
Self { client }
}
pub fn from(&self, id: &str) -> StorageBucket {
StorageBucket::new(self.client.clone(), id)
}
pub async fn list_buckets(&self) -> Result<Vec<Bucket>> {
let url = format!("{}/storage/v1/bucket", self.client.inner.url);
let resp = self
.client
.inner
.http
.get(&url)
.header("apikey", &self.client.inner.key)
.header("Authorization", self.client.auth_header())
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(SupaError::ApiError {
code: status.as_u16(),
message: format!("List Buckets Error: {}", text),
details: None,
});
}
let buckets = resp.json().await?;
Ok(buckets)
}
}
pub struct StorageBucket {
client: SupabaseClient,
pub id: String,
}
impl StorageBucket {
pub(crate) fn new(client: SupabaseClient, id: &str) -> Self {
Self {
client,
id: id.to_string(),
}
}
pub async fn upload<P: AsRef<Path>>(
&self,
path: &str,
file_path: P,
mime_type: Option<&str>,
) -> Result<UploadedFile> {
let path_ref = file_path.as_ref();
let filename = path_ref
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| SupaError::ClientError {
message: format!("Invalid filename for path: {}", path_ref.display()),
})?
.to_string();
let file_bytes = tokio::fs::read(path_ref)
.await
.map_err(|e| SupaError::ClientError {
message: format!("Failed to read file {}: {}", path_ref.display(), e),
})?;
let mime = if let Some(m) = mime_type {
m.to_string()
} else {
mime_guess::from_path(path_ref)
.first_or_octet_stream()
.to_string()
};
let part = reqwest::multipart::Part::bytes(file_bytes)
.file_name(filename)
.mime_str(&mime)?;
let form = multipart::Form::new().part("file", part);
let url = format!(
"{}/storage/v1/object/{}/{}",
self.client.inner.url, self.id, path
);
let resp = self
.client
.inner
.http
.post(&url)
.header("apikey", &self.client.inner.key)
.header("Authorization", self.client.auth_header())
.multipart(form)
.send()
.await?;
let status = resp.status();
if !status.is_success() {
let error_text = resp.text().await.unwrap_or_default();
return Err(SupaError::ApiError {
code: status.as_u16(),
message: format!("Upload Error: {}", error_text),
details: None,
});
}
let uploaded: UploadedFile = resp.json().await?;
Ok(uploaded)
}
pub async fn download(&self, path: &str) -> Result<Vec<u8>> {
let url = format!(
"{}/storage/v1/object/{}/{}",
self.client.inner.url, self.id, path
);
let resp = self
.client
.inner
.http
.get(&url)
.header("apikey", &self.client.inner.key)
.header("Authorization", self.client.auth_header())
.send()
.await?;
let status = resp.status();
if !status.is_success() {
let error_text = resp.text().await.unwrap_or_default();
return Err(SupaError::ApiError {
code: status.as_u16(),
message: format!("Download Error: {}", error_text),
details: None,
});
}
let bytes = resp.bytes().await?;
Ok(bytes.to_vec())
}
pub fn get_public_url(&self, path: &str) -> Result<String> {
let path_url = format!("storage/v1/object/public/{}/{}", self.id, path);
let url = self.client.inner.url.join(&path_url)?;
Ok(url.to_string())
}
pub async fn create_signed_url(&self, path: &str, expires_in: i32) -> Result<String> {
let url = format!(
"{}/storage/v1/object/sign/{}/{}",
self.client.inner.url, self.id, path
);
let body = serde_json::json!({
"expiresIn": expires_in,
});
let resp = self
.client
.inner
.http
.post(&url)
.header("apikey", &self.client.inner.key)
.header("Authorization", self.client.auth_header())
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(SupaError::ApiError {
code: status.as_u16(),
message: format!("Create Signed URL Error: {}", text),
details: None,
});
}
let json: serde_json::Value = resp.json().await?;
let signed_path = json
.get("signedURL")
.and_then(|v| v.as_str())
.ok_or_else(|| SupaError::ApiError {
code: 500,
message: "Missing signedURL in response".to_string(),
details: None,
})?;
let full_url = self.client.inner.url.join(signed_path)?;
Ok(full_url.to_string())
}
pub async fn delete(&self, path: &str) -> Result<()> {
let url = format!(
"{}/storage/v1/object/{}/{}",
self.client.inner.url, self.id, path
);
let resp = self
.client
.inner
.http
.delete(&url)
.header("apikey", &self.client.inner.key)
.header("Authorization", self.client.auth_header())
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(SupaError::ApiError {
code: status.as_u16(),
message: format!("Delete Error: {}", text),
details: None,
});
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct Bucket {
pub id: String,
pub name: String,
pub public: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Deserialize)]
pub struct UploadedFile {
pub id: String,
pub key: String,
}