use bytes::Bytes;
use reqwest::header::{CACHE_CONTROL, CONTENT_TYPE};
use reqwest::{Body, Method};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::error::StorageError;
use crate::types::{FileObject, FileOptions, ListOptions, ObjectKey, SignedUrl};
use crate::{StorageClient, path};
#[derive(Debug, Clone, Copy)]
pub struct Bucket<'a> {
client: &'a StorageClient,
id: &'a str,
}
#[derive(Serialize)]
struct ListBody<'a> {
prefix: &'a str,
#[serde(flatten)]
options: &'a ListOptions,
}
#[derive(Serialize)]
struct RemoveBody<'a> {
prefixes: &'a [&'a str],
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct TransferBody<'a> {
bucket_id: &'a str,
source_key: &'a str,
destination_key: &'a str,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SignBody<'a> {
expires_in: u64,
paths: &'a [&'a str],
}
#[derive(Deserialize)]
struct SignedEntry {
#[serde(default)]
path: Option<String>,
#[serde(rename = "signedURL", default)]
signed_url: Option<String>,
#[serde(default)]
error: Option<String>,
}
impl<'a> Bucket<'a> {
pub(crate) const fn new(client: &'a StorageClient, id: &'a str) -> Self {
Self { client, id }
}
#[must_use]
pub const fn id(&self) -> &'a str {
self.id
}
pub async fn upload<B: Into<Body>>(
&self,
path: &str,
body: B,
options: &FileOptions,
) -> Result<ObjectKey, StorageError> {
self.write(Method::POST, path, body.into(), options).await
}
pub async fn update<B: Into<Body>>(
&self,
path: &str,
body: B,
options: &FileOptions,
) -> Result<ObjectKey, StorageError> {
self.write(Method::PUT, path, body.into(), options).await
}
async fn write(
&self,
method: Method,
path: &str,
body: Body,
options: &FileOptions,
) -> Result<ObjectKey, StorageError> {
let url = self.object_url(&["object"], path)?;
let upsert = if options.upsert { "true" } else { "false" };
let mut request = self
.client
.request(method, url)
.header("x-upsert", upsert)
.body(body);
if let Some(content_type) = &options.content_type {
request = request.header(CONTENT_TYPE, content_type.as_str());
}
request = request.header(CACHE_CONTROL, format!("max-age={}", options.cache_control));
StorageClient::json(request).await
}
pub async fn download(&self, path: &str) -> Result<Bytes, StorageError> {
Ok(self.download_stream(path).await?.response.bytes().await?)
}
pub async fn download_stream(&self, path: &str) -> Result<ObjectDownload, StorageError> {
let url = self.object_url(&["object"], path)?;
let response = StorageClient::send(self.client.request(Method::GET, url)).await?;
Ok(ObjectDownload { response })
}
pub async fn list(
&self,
prefix: &str,
options: &ListOptions,
) -> Result<Vec<FileObject>, StorageError> {
let url = self.client.url(["object", "list", path::bucket(self.id)?]);
let body = ListBody { prefix, options };
StorageClient::json(self.client.request(Method::POST, url).json(&body)).await
}
pub async fn remove(&self, paths: &[&str]) -> Result<Vec<FileObject>, StorageError> {
for object in paths {
path::object(object)?;
}
let url = self.client.url(["object", path::bucket(self.id)?]);
let body = RemoveBody { prefixes: paths };
StorageClient::json(self.client.request(Method::DELETE, url).json(&body)).await
}
pub async fn remove_object(&self, path: &str) -> Result<(), StorageError> {
let url = self.object_url(&["object"], path)?;
StorageClient::send(self.client.request(Method::DELETE, url)).await?;
Ok(())
}
pub async fn move_object(&self, from: &str, to: &str) -> Result<(), StorageError> {
let request = self.transfer("move", from, to)?;
StorageClient::send(request).await?;
Ok(())
}
pub async fn copy_object(&self, from: &str, to: &str) -> Result<ObjectKey, StorageError> {
StorageClient::json(self.transfer("copy", from, to)?).await
}
fn transfer(
&self,
action: &str,
from: &str,
to: &str,
) -> Result<reqwest::RequestBuilder, StorageError> {
path::object(from)?;
path::object(to)?;
let body = TransferBody {
bucket_id: path::bucket(self.id)?,
source_key: from,
destination_key: to,
};
let url = self.client.url(["object", action]);
Ok(self.client.request(Method::POST, url).json(&body))
}
pub async fn create_signed_url(
&self,
path: &str,
expires_in: u64,
) -> Result<Url, StorageError> {
let entry = self
.create_signed_urls(&[path], expires_in)
.await?
.into_iter()
.next();
match entry {
Some(SignedUrl { url: Some(url), .. }) => Ok(url),
Some(SignedUrl { error, .. }) => Err(StorageError::SignFailed {
path: path.to_owned(),
message: error.unwrap_or_default(),
}),
None => Err(StorageError::SignFailed {
path: path.to_owned(),
message: "the response has no entry".to_owned(),
}),
}
}
pub async fn create_signed_urls(
&self,
paths: &[&str],
expires_in: u64,
) -> Result<Vec<SignedUrl>, StorageError> {
for object in paths {
path::object(object)?;
}
let url = self.client.url(["object", "sign", path::bucket(self.id)?]);
let body = SignBody { expires_in, paths };
let entries: Vec<SignedEntry> =
StorageClient::json(self.client.request(Method::POST, url).json(&body)).await?;
entries
.into_iter()
.zip(paths)
.map(|(entry, requested)| {
let url = entry
.signed_url
.map(|signed| {
self.signed_url(entry.path.as_deref().unwrap_or(requested), &signed)
})
.transpose()?;
Ok(SignedUrl {
path: entry.path,
url,
error: entry.error,
})
})
.collect()
}
fn signed_url(&self, path: &str, signed: &str) -> Result<Url, StorageError> {
let (_, token) = signed
.rsplit_once("?token=")
.ok_or_else(|| StorageError::MissingSignedToken(signed.to_owned()))?;
let mut url = self.object_url(&["object", "sign"], path)?;
url.set_query(Some(&format!("token={token}")));
Ok(url)
}
pub fn public_url(&self, path: &str) -> Result<Url, StorageError> {
self.object_url(&["object", "public"], path)
}
fn object_url(&self, prefix: &[&str], path: &str) -> Result<Url, StorageError> {
let bucket = path::bucket(self.id)?;
let segments = path::object(path)?;
Ok(self.client.url(
prefix
.iter()
.copied()
.chain(core::iter::once(bucket))
.chain(segments),
))
}
}
#[derive(Debug)]
pub struct ObjectDownload {
response: reqwest::Response,
}
impl ObjectDownload {
#[must_use]
pub fn content_length(&self) -> Option<u64> {
self.response.content_length()
}
pub async fn chunk(&mut self) -> Result<Option<Bytes>, StorageError> {
Ok(self.response.chunk().await?)
}
pub fn into_stream(
self,
) -> impl futures_util::Stream<Item = Result<Bytes, StorageError>> + Send + 'static {
futures_util::TryStreamExt::map_err(self.response.bytes_stream(), StorageError::from)
}
}