use reqwest::header::HeaderValue;
use serde_json::json;
use crate::client::StorageClient;
use crate::error::StorageError;
use crate::types::*;
#[derive(Debug, Clone)]
pub struct StorageBucketApi {
client: StorageClient,
bucket_id: String,
}
impl StorageBucketApi {
pub(crate) fn new(client: StorageClient, bucket_id: String) -> Self {
Self { client, bucket_id }
}
pub async fn upload(
&self,
path: &str,
data: Vec<u8>,
options: FileOptions,
) -> Result<UploadResponse, StorageError> {
let url = self
.client
.url(&format!("/object/{}/{}", self.bucket_id, path));
let content_type = options
.content_type
.as_deref()
.unwrap_or("application/octet-stream");
let mut req = self
.client
.http()
.post(url)
.header("content-type", content_type)
.body(data);
if let Some(cache) = &options.cache_control {
req = req.header("cache-control", cache.as_str());
}
if let Some(upsert) = options.upsert {
req = req.header("x-upsert", if upsert { "true" } else { "false" });
}
if let Some(metadata) = &options.metadata {
let meta_str = serde_json::to_string(metadata)?;
req = req.header("x-metadata", meta_str);
}
let resp = req.send().await?;
self.client.handle_response(resp).await
}
pub async fn update(
&self,
path: &str,
data: Vec<u8>,
options: Option<FileOptions>,
) -> Result<UploadResponse, StorageError> {
let url = self
.client
.url(&format!("/object/{}/{}", self.bucket_id, path));
let opts = options.unwrap_or_default();
let content_type = opts
.content_type
.as_deref()
.unwrap_or("application/octet-stream");
let mut req = self
.client
.http()
.put(url)
.header("content-type", content_type)
.body(data);
if let Some(cache) = &opts.cache_control {
req = req.header("cache-control", cache.as_str());
}
if let Some(upsert) = opts.upsert {
req = req.header("x-upsert", if upsert { "true" } else { "false" });
}
if let Some(metadata) = &opts.metadata {
let meta_str = serde_json::to_string(metadata)?;
req = req.header("x-metadata", meta_str);
}
let resp = req.send().await?;
self.client.handle_response(resp).await
}
pub async fn download(&self, path: &str) -> Result<Vec<u8>, StorageError> {
let url = self
.client
.url(&format!("/object/{}/{}", self.bucket_id, path));
let resp = self.client.http().get(url).send().await?;
self.client.handle_bytes_response(resp).await
}
pub async fn list(
&self,
path: Option<&str>,
options: Option<SearchOptions>,
) -> Result<Vec<FileObject>, StorageError> {
let url = self
.client
.url(&format!("/object/list/{}", self.bucket_id));
let mut body = json!({
"prefix": path.unwrap_or(""),
});
if let Some(opts) = options {
if let Some(limit) = opts.limit {
body["limit"] = json!(limit);
}
if let Some(offset) = opts.offset {
body["offset"] = json!(offset);
}
if let Some(sort_by) = opts.sort_by {
body["sortBy"] = json!(sort_by);
}
if let Some(search) = opts.search {
body["search"] = json!(search);
}
}
let resp = self.client.http().post(url).json(&body).send().await?;
self.client.handle_response(resp).await
}
pub async fn move_file(&self, from: &str, to: &str) -> Result<(), StorageError> {
let url = self.client.url("/object/move");
let body = json!({
"bucketId": self.bucket_id,
"sourceKey": from,
"destinationKey": to,
});
let resp = self.client.http().post(url).json(&body).send().await?;
self.client.handle_empty_response(resp).await
}
pub async fn copy(&self, from: &str, to: &str) -> Result<String, StorageError> {
let url = self.client.url("/object/copy");
let body = json!({
"bucketId": self.bucket_id,
"sourceKey": from,
"destinationKey": to,
});
let resp = self.client.http().post(url).json(&body).send().await?;
let result: serde_json::Value = self.client.handle_response(resp).await?;
Ok(result
.get("Key")
.or_else(|| result.get("key"))
.and_then(|v| v.as_str())
.unwrap_or(to)
.to_string())
}
pub async fn remove(&self, paths: Vec<&str>) -> Result<Vec<FileObject>, StorageError> {
let url = self
.client
.url(&format!("/object/{}", self.bucket_id));
let body = json!({
"prefixes": paths,
});
let resp = self.client.http().delete(url).json(&body).send().await?;
self.client.handle_response(resp).await
}
pub async fn create_signed_url(
&self,
path: &str,
expires_in: u64,
) -> Result<SignedUrlResponse, StorageError> {
let url = self
.client
.url(&format!("/object/sign/{}/{}", self.bucket_id, path));
let body = json!({ "expiresIn": expires_in });
let resp = self.client.http().post(url).json(&body).send().await?;
let mut result: SignedUrlResponse = self.client.handle_response(resp).await?;
if result.signed_url.starts_with('/') {
let base = self.client.base_url().as_str().trim_end_matches('/');
result.signed_url = format!("{}{}", base, result.signed_url);
}
Ok(result)
}
pub async fn create_signed_urls(
&self,
paths: Vec<&str>,
expires_in: u64,
) -> Result<Vec<SignedUrlBatchEntry>, StorageError> {
let url = self
.client
.url(&format!("/object/sign/{}", self.bucket_id));
let body = json!({
"expiresIn": expires_in,
"paths": paths,
});
let resp = self.client.http().post(url).json(&body).send().await?;
let mut results: Vec<SignedUrlBatchEntry> = self.client.handle_response(resp).await?;
let base = self.client.base_url().as_str().trim_end_matches('/');
for entry in &mut results {
if let Some(ref mut signed_url) = entry.signed_url {
if signed_url.starts_with('/') {
*signed_url = format!("{}{}", base, signed_url);
}
}
}
Ok(results)
}
pub fn get_public_url(&self, path: &str) -> String {
let base = self.client.base_url().as_str().trim_end_matches('/');
format!("{}/object/public/{}/{}", base, self.bucket_id, path)
}
pub async fn create_signed_upload_url(
&self,
path: &str,
) -> Result<SignedUploadUrlResponse, StorageError> {
let url = self.client.url(&format!(
"/object/upload/sign/{}/{}",
self.bucket_id, path
));
let resp = self
.client
.http()
.post(url)
.json(&json!({}))
.send()
.await?;
self.client.handle_response(resp).await
}
pub async fn upload_to_signed_url(
&self,
token: &str,
path: &str,
data: Vec<u8>,
options: Option<FileOptions>,
) -> Result<(), StorageError> {
let url = self.client.url(&format!(
"/object/upload/sign/{}/{}?token={}",
self.bucket_id, path, token
));
let opts = options.unwrap_or_default();
let content_type = opts
.content_type
.as_deref()
.unwrap_or("application/octet-stream");
let mut req = self
.client
.http()
.put(url)
.header("content-type", content_type)
.body(data);
if let Some(cache) = &opts.cache_control {
req = req.header(
"cache-control",
HeaderValue::from_str(cache)
.map_err(|e| StorageError::InvalidConfig(format!("Invalid header: {}", e)))?,
);
}
let resp = req.send().await?;
self.client.handle_empty_response(resp).await
}
pub async fn info(&self, path: &str) -> Result<FileInfo, StorageError> {
let url = self.client.url(&format!(
"/object/info/authenticated/{}/{}",
self.bucket_id, path
));
let resp = self.client.http().get(url).send().await?;
self.client.handle_response(resp).await
}
pub async fn exists(&self, path: &str) -> Result<bool, StorageError> {
let url = self
.client
.url(&format!("/object/{}/{}", self.bucket_id, path));
let resp = self.client.http().head(url).send().await?;
let status = resp.status().as_u16();
if status >= 200 && status < 300 {
Ok(true)
} else if status == 404 || status == 400 {
Ok(false)
} else {
Err(StorageError::Api {
status,
message: format!("HTTP {}", status),
})
}
}
pub async fn download_with_transform(
&self,
path: &str,
transform: &TransformOptions,
) -> Result<Vec<u8>, StorageError> {
let qs = transform.to_query_string();
let url_path = if qs.is_empty() {
format!(
"/render/image/authenticated/{}/{}",
self.bucket_id, path
)
} else {
format!(
"/render/image/authenticated/{}/{}?{}",
self.bucket_id, path, qs
)
};
let url = self.client.url(&url_path);
let resp = self.client.http().get(url).send().await?;
self.client.handle_bytes_response(resp).await
}
pub fn get_public_url_with_transform(
&self,
path: &str,
transform: &TransformOptions,
) -> String {
let base = self.client.base_url().as_str().trim_end_matches('/');
let qs = transform.to_query_string();
if qs.is_empty() {
format!(
"{}/render/image/public/{}/{}",
base, self.bucket_id, path
)
} else {
format!(
"{}/render/image/public/{}/{}?{}",
base, self.bucket_id, path, qs
)
}
}
pub async fn create_signed_url_with_transform(
&self,
path: &str,
expires_in: u64,
transform: &TransformOptions,
) -> Result<SignedUrlResponse, StorageError> {
let url = self
.client
.url(&format!("/object/sign/{}/{}", self.bucket_id, path));
let mut body = json!({ "expiresIn": expires_in });
if !transform.is_empty() {
body["transform"] = transform.to_json();
}
let resp = self.client.http().post(url).json(&body).send().await?;
let mut result: SignedUrlResponse = self.client.handle_response(resp).await?;
if result.signed_url.starts_with('/') {
let base = self.client.base_url().as_str().trim_end_matches('/');
result.signed_url = format!("{}{}", base, result.signed_url);
}
Ok(result)
}
pub async fn create_signed_urls_with_transform(
&self,
paths: Vec<&str>,
expires_in: u64,
transform: &TransformOptions,
) -> Result<Vec<SignedUrlBatchEntry>, StorageError> {
let url = self
.client
.url(&format!("/object/sign/{}", self.bucket_id));
let mut body = json!({
"expiresIn": expires_in,
"paths": paths,
});
if !transform.is_empty() {
body["transform"] = transform.to_json();
}
let resp = self.client.http().post(url).json(&body).send().await?;
let mut results: Vec<SignedUrlBatchEntry> = self.client.handle_response(resp).await?;
let base = self.client.base_url().as_str().trim_end_matches('/');
for entry in &mut results {
if let Some(ref mut signed_url) = entry.signed_url {
if signed_url.starts_with('/') {
*signed_url = format!("{}{}", base, signed_url);
}
}
}
Ok(results)
}
pub async fn move_to_bucket(
&self,
from: &str,
to_bucket: &str,
to_path: &str,
) -> Result<(), StorageError> {
let url = self.client.url("/object/move");
let body = json!({
"bucketId": self.bucket_id,
"sourceKey": from,
"destinationBucket": to_bucket,
"destinationKey": to_path,
});
let resp = self.client.http().post(url).json(&body).send().await?;
self.client.handle_empty_response(resp).await
}
pub async fn copy_to_bucket(
&self,
from: &str,
to_bucket: &str,
to_path: &str,
) -> Result<String, StorageError> {
let url = self.client.url("/object/copy");
let body = json!({
"bucketId": self.bucket_id,
"sourceKey": from,
"destinationBucket": to_bucket,
"destinationKey": to_path,
});
let resp = self.client.http().post(url).json(&body).send().await?;
let result: serde_json::Value = self.client.handle_response(resp).await?;
Ok(result
.get("Key")
.or_else(|| result.get("key"))
.and_then(|v| v.as_str())
.unwrap_or(to_path)
.to_string())
}
pub fn get_public_url_with_download(&self, path: &str, filename: Option<&str>) -> String {
let base_url = self.get_public_url(path);
match filename {
Some(name) => format!("{}?download={}", base_url, name),
None => base_url,
}
}
pub async fn create_signed_url_with_download(
&self,
path: &str,
expires_in: u64,
filename: Option<&str>,
) -> Result<SignedUrlResponse, StorageError> {
let mut result = self.create_signed_url(path, expires_in).await?;
if let Some(name) = filename {
let separator = if result.signed_url.contains('?') { "&" } else { "?" };
result.signed_url = format!("{}{}download={}", result.signed_url, separator, name);
}
Ok(result)
}
pub fn bucket_id(&self) -> &str {
&self.bucket_id
}
}