mod bucket_api;
pub mod types;
pub use bucket_api::BucketApi;
pub use types::{
Bucket, CreateBucketOptions, FileObject, ImageFormat, ImageResize, ListOptions,
PublicUrlOptions, SignedUrlEntry, SignedUploadUrl, SortColumn, SortOrder, TransformOptions,
UpdateBucketOptions, UploadOptions, UploadResponse,
};
use serde_json::Value;
use crate::error::{Result, SupabaseError};
use crate::universals::{HttpMethod, RequestOptions, Service};
use crate::SupabaseClient;
impl SupabaseClient {
pub fn storage(&self) -> Storage {
Storage { client: self.clone() }
}
}
#[derive(Debug, Clone)]
pub struct Storage {
pub(crate) client: SupabaseClient,
}
fn storage_opts() -> RequestOptions {
RequestOptions {
service: Some(Service::Storage),
..RequestOptions::default()
}
}
impl Storage {
pub fn from(&self, bucket: impl Into<String>) -> BucketApi {
BucketApi::new(self.client.clone(), bucket.into())
}
pub async fn list_buckets(&self) -> Result<Vec<Bucket>> {
let value = self
.client
.request_with("/storage/v1/bucket", HttpMethod::Get, None, &storage_opts())
.await?;
decode_json(value)
}
pub async fn get_bucket(&self, id: &str) -> Result<Bucket> {
let value = self
.client
.request_with(
&format!("/storage/v1/bucket/{id}"),
HttpMethod::Get,
None,
&storage_opts(),
)
.await?;
decode_json(value)
}
pub async fn create_bucket(&self, id: &str, options: CreateBucketOptions) -> Result<String> {
let mut body = serde_json::to_value(&options)
.map_err(|e| SupabaseError::Unexpected(format!("serialize options: {e}")))?;
if let Value::Object(map) = &mut body {
map.insert("id".to_string(), Value::String(id.to_string()));
map.insert("name".to_string(), Value::String(id.to_string()));
}
let value = self
.client
.request_with(
"/storage/v1/bucket",
HttpMethod::Post,
Some(body),
&storage_opts(),
)
.await?;
value
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| SupabaseError::Unexpected(format!("create_bucket response: {value}")))
}
pub async fn update_bucket(&self, id: &str, options: UpdateBucketOptions) -> Result<()> {
let body = serde_json::to_value(&options)
.map_err(|e| SupabaseError::Unexpected(format!("serialize options: {e}")))?;
self.client
.request_with(
&format!("/storage/v1/bucket/{id}"),
HttpMethod::Put,
Some(body),
&storage_opts(),
)
.await?;
Ok(())
}
pub async fn empty_bucket(&self, id: &str) -> Result<()> {
self.client
.request_with(
&format!("/storage/v1/bucket/{id}/empty"),
HttpMethod::Post,
None,
&storage_opts(),
)
.await?;
Ok(())
}
pub async fn delete_bucket(&self, id: &str) -> Result<()> {
self.client
.request_with(
&format!("/storage/v1/bucket/{id}"),
HttpMethod::Delete,
None,
&storage_opts(),
)
.await?;
Ok(())
}
}
fn decode_json<T: serde::de::DeserializeOwned>(value: Value) -> Result<T> {
serde_json::from_value(value.clone()).map_err(|e| SupabaseError::Decode {
message: e.to_string(),
body: value.to_string(),
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn decode_json_success_returns_value() {
let v: Bucket = decode_json(json!({
"id": "b", "name": "b", "owner": null, "public": false,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}))
.unwrap();
assert_eq!(v.id, "b");
}
#[test]
fn decode_json_failure_produces_decode_error() {
let err = decode_json::<Vec<Bucket>>(json!("not an array")).unwrap_err();
match err {
SupabaseError::Decode { message, body } => {
assert!(!message.is_empty(), "message should be populated");
assert_eq!(body, "\"not an array\"");
}
other => panic!("expected Decode error, got {other:?}"),
}
}
}