use crate::client::{AuraClient, RequestBody};
use crate::error::AuraError;
use crate::types::{AuraResponse, FileEntry, ObjectMeta, StorageBucket, UploadResult};
pub struct StorageService {
client: AuraClient,
}
#[derive(Debug, Clone, Default)]
pub struct UploadOptions {
pub content_type: Option<String>,
pub upsert: Option<bool>,
}
impl StorageService {
pub fn new(client: AuraClient) -> Self {
Self { client }
}
fn prefix(&self) -> String {
"/v1/storage".to_string()
}
pub async fn list_buckets(&self) -> Result<AuraResponse<Vec<StorageBucket>>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/buckets", self.prefix()),
RequestBody::None,
)
.await
}
pub async fn get_bucket(&self, id: &str) -> Result<AuraResponse<StorageBucket>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/buckets/{}", self.prefix(), id),
RequestBody::None,
)
.await
}
pub async fn create_bucket(
&self,
name: &str,
public: Option<bool>,
file_size_limit: Option<u64>,
allowed_mime_types: Option<Vec<String>>,
) -> Result<AuraResponse<StorageBucket>, AuraError> {
let body = serde_json::json!({
"name": name,
"public": public.unwrap_or(false),
"file_size_limit": file_size_limit,
"allowed_mime_types": allowed_mime_types,
});
self.client
.request(
reqwest::Method::POST,
&format!("{}/buckets", self.prefix()),
RequestBody::Json(body),
)
.await
}
pub async fn delete_bucket(
&self,
id: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::DELETE,
&format!("{}/buckets/{}", self.prefix(), id),
RequestBody::None,
)
.await
}
pub async fn upload(
&self,
bucket_id: &str,
path: &str,
file_data: Vec<u8>,
options: Option<UploadOptions>,
) -> Result<AuraResponse<UploadResult>, AuraError> {
let mut form = reqwest::multipart::Form::new();
let mime = options
.as_ref()
.and_then(|o| o.content_type.clone())
.unwrap_or_else(|| "application/octet-stream".to_string());
let part = reqwest::multipart::Part::bytes(file_data)
.file_name(path.to_string())
.mime_str(&mime)
.map_err(|e| AuraError::serialization(&e.to_string()))?;
form = form.part("file", part);
if let Some(opts) = options {
if opts.upsert.unwrap_or(false) {
form = form.text("upsert", "true");
}
}
self.client
.request(
reqwest::Method::POST,
&format!("{}/{}", self.prefix(), bucket_id),
RequestBody::Multipart(form),
)
.await
}
pub async fn download(
&self,
bucket_id: &str,
path: &str,
) -> Result<AuraResponse<Vec<u8>>, AuraError> {
let base = self.client.base_url().trim_end_matches('/');
let prefix = self.prefix();
let prefix_clean = prefix.trim_start_matches('/');
let full_url = format!("{}/{}/{}/{}", base, prefix_clean, bucket_id, path);
let mut builder = self.client.inner.http_client.get(&full_url);
if let Some(ref api_key) = self.client.inner.api_key {
builder = builder.header("apikey", api_key);
}
if let Some(token) = self.client.inner.auth_store.token() {
builder = builder.header("Authorization", format!("Bearer {}", token));
}
let res = builder
.send()
.await
.map_err(|e| AuraError::network(&e.to_string()))?;
let status = res.status().as_u16();
if !res.status().is_success() {
let body_val: Option<serde_json::Value> = res.json().await.ok();
let mut code = format!("http_{}", status);
let mut message = format!("HTTP {}", status);
let mut details = None;
if let Some(ref body) = body_val {
details = Some(body.clone());
if let Some(err_obj) = body.get("error") {
if let Some(c) = err_obj.get("code").and_then(|v| v.as_str()) {
code = c.to_string();
}
if let Some(m) = err_obj.get("message").and_then(|v| v.as_str()) {
message = m.to_string();
}
}
}
return Ok(AuraResponse {
data: None,
error: Some(AuraError::new(status, &code, &message, details)),
meta: None,
});
}
let bytes = res
.bytes()
.await
.map_err(|e| AuraError::network(&e.to_string()))?;
Ok(AuraResponse {
data: Some(bytes.to_vec()),
error: None,
meta: None,
})
}
pub async fn list(
&self,
bucket_id: &str,
prefix: Option<&str>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<AuraResponse<Vec<FileEntry>>, AuraError> {
let mut query = Vec::new();
if let Some(p) = prefix {
query.push(format!("prefix={}", urlencoding::encode(p)));
}
if let Some(l) = limit {
query.push(format!("limit={}", l));
}
if let Some(o) = offset {
query.push(format!("offset={}", o));
}
let qs = if query.is_empty() {
"".to_string()
} else {
format!("?{}", query.join("&"))
};
let mut res: AuraResponse<Vec<FileEntry>> = self
.client
.request(
reqwest::Method::GET,
&format!("{}/{}{}", self.prefix(), bucket_id, qs),
RequestBody::None,
)
.await?;
if let Some(entries) = res.data.as_mut() {
let bucket_prefix = format!("{}/", bucket_id);
for entry in entries.iter_mut() {
if let Some(stripped) = entry.name.strip_prefix(bucket_prefix.as_str()) {
entry.name = stripped.to_string();
}
}
}
Ok(res)
}
pub async fn remove(
&self,
bucket_id: &str,
paths: Vec<String>,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
if paths.len() == 1 {
return self
.client
.request(
reqwest::Method::DELETE,
&format!("{}/{}/{}", self.prefix(), bucket_id, paths[0]),
RequestBody::None,
)
.await;
}
let mut last_res = AuraResponse {
data: None,
error: None,
meta: None,
};
for p in paths {
let res = self
.client
.request::<serde_json::Value>(
reqwest::Method::DELETE,
&format!("{}/{}/{}", self.prefix(), bucket_id, p),
RequestBody::None,
)
.await?;
if res.error.is_some() {
return Ok(res);
}
last_res = res;
}
Ok(last_res)
}
pub async fn copy(
&self,
bucket_id: &str,
from_path: &str,
to_path: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let body = serde_json::json!({
"src_key": from_path,
"dst_key": to_path,
});
self.client
.request(
reqwest::Method::POST,
&format!("{}/{}/copy", self.prefix(), bucket_id),
RequestBody::Json(body),
)
.await
}
pub async fn move_object(
&self,
bucket_id: &str,
from_path: &str,
to_path: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let body = serde_json::json!({
"src_key": from_path,
"dst_key": to_path,
});
self.client
.request(
reqwest::Method::POST,
&format!("{}/{}/move", self.prefix(), bucket_id),
RequestBody::Json(body),
)
.await
}
pub async fn get_metadata(
&self,
bucket_id: &str,
path: &str,
) -> Result<AuraResponse<ObjectMeta>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/{}/meta/{}", self.prefix(), bucket_id, path),
RequestBody::None,
)
.await
}
pub async fn create_signed_url(
&self,
bucket_id: &str,
path: &str,
expires_in_seconds: Option<u32>,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let ttl = expires_in_seconds.unwrap_or(3600);
self.client
.request(
reqwest::Method::GET,
&format!(
"{}/{}/signed-url?key={}&ttl_secs={}",
self.prefix(),
bucket_id,
urlencoding::encode(path),
ttl
),
RequestBody::None,
)
.await
}
pub async fn create_signed_upload_url(
&self,
bucket_id: &str,
path: &str,
expires_in_seconds: Option<u32>,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let ttl = expires_in_seconds.unwrap_or(3600);
self.client
.request(
reqwest::Method::GET,
&format!(
"{}/{}/upload-url?key={}&ttl_secs={}",
self.prefix(),
bucket_id,
urlencoding::encode(path),
ttl
),
RequestBody::None,
)
.await
}
pub fn get_public_url(&self, bucket_id: &str, path: &str) -> String {
let base = self.client.base_url().trim_end_matches('/');
let prefix = self.prefix();
let prefix_clean = prefix.trim_start_matches('/');
format!("{}/{}/{}/{}", base, prefix_clean, bucket_id, path)
}
}