use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use reqwest::{Method, RequestBuilder, Response};
use serde::Serialize;
use serde::de::DeserializeOwned;
use url::Url;
use crate::error::{ApiErrorBody, StorageError};
use crate::types::{BucketInfo, BucketOptions};
use crate::{Bucket, path};
fn same_origin_redirects() -> reqwest::redirect::Policy {
reqwest::redirect::Policy::custom(|attempt| {
let same_origin = attempt
.previous()
.last()
.is_some_and(|prev| prev.origin() == attempt.url().origin());
if !same_origin {
attempt.stop()
} else if attempt.previous().len() > 10 {
attempt.error("too many redirects")
} else {
attempt.follow()
}
})
}
#[derive(Debug, Clone)]
pub struct StorageClient {
http: reqwest::Client,
base: Url,
headers: HeaderMap,
}
#[derive(Serialize)]
struct BucketBody<'a> {
id: &'a str,
name: &'a str,
#[serde(flatten)]
options: &'a BucketOptions,
}
impl StorageClient {
pub fn new(project_url: &Url, api_key: &str) -> Result<Self, StorageError> {
let http = reqwest::Client::builder()
.redirect(same_origin_redirects())
.build()?;
Self::new_with_client(project_url, api_key, http)
}
pub fn new_with_client(
project_url: &Url,
api_key: &str,
http: reqwest::Client,
) -> Result<Self, StorageError> {
let base = storage_base(project_url)?;
let mut headers = HeaderMap::new();
headers.insert("apikey", sensitive(api_key)?);
if !(api_key.starts_with("sb_publishable_") || api_key.starts_with("sb_secret_")) {
headers.insert(AUTHORIZATION, sensitive(&format!("Bearer {api_key}"))?);
}
Ok(Self {
http,
base,
headers,
})
}
pub fn with_access_token(&self, token: &str) -> Result<Self, StorageError> {
let mut next = self.clone();
next.headers
.insert(AUTHORIZATION, sensitive(&format!("Bearer {token}"))?);
Ok(next)
}
pub fn with_project_url(&self, project_url: &Url) -> Result<Self, StorageError> {
Ok(Self {
http: self.http.clone(),
base: storage_base(project_url)?,
headers: self.headers.clone(),
})
}
#[must_use]
pub const fn from<'a>(&'a self, bucket: &'a str) -> Bucket<'a> {
Bucket::new(self, bucket)
}
pub async fn list_buckets(&self) -> Result<Vec<BucketInfo>, StorageError> {
Self::json(self.request(Method::GET, self.url(["bucket"]))).await
}
pub async fn get_bucket(&self, id: &str) -> Result<BucketInfo, StorageError> {
let url = self.url(["bucket", path::bucket(id)?]);
Self::json(self.request(Method::GET, url)).await
}
pub async fn create_bucket(
&self,
id: &str,
options: &BucketOptions,
) -> Result<(), StorageError> {
let body = BucketBody {
id: path::bucket(id)?,
name: id,
options,
};
Self::send(self.request(Method::POST, self.url(["bucket"])).json(&body)).await?;
Ok(())
}
pub async fn update_bucket(
&self,
id: &str,
options: &BucketOptions,
) -> Result<(), StorageError> {
let url = self.url(["bucket", path::bucket(id)?]);
let body = BucketBody {
id,
name: id,
options,
};
Self::send(self.request(Method::PUT, url).json(&body)).await?;
Ok(())
}
pub async fn empty_bucket(&self, id: &str) -> Result<(), StorageError> {
let url = self.url(["bucket", path::bucket(id)?, "empty"]);
Self::send(
self.request(Method::POST, url)
.json(&serde_json::Map::new()),
)
.await?;
Ok(())
}
pub async fn delete_bucket(&self, id: &str) -> Result<(), StorageError> {
let url = self.url(["bucket", path::bucket(id)?]);
Self::send(self.request(Method::DELETE, url)).await?;
Ok(())
}
pub(crate) fn url<'a, I: IntoIterator<Item = &'a str>>(&self, segments: I) -> Url {
let mut url = self.base.clone();
if let Ok(mut path) = url.path_segments_mut() {
path.pop_if_empty().extend(segments);
}
url
}
pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder {
self.http.request(method, url).headers(self.headers.clone())
}
pub(crate) async fn send(request: RequestBuilder) -> Result<Response, StorageError> {
let response = request.send().await?;
let status = response.status();
if status.is_success() {
return Ok(response);
}
let body = ApiErrorBody::decode(response.text().await?);
Err(StorageError::Api { status, body })
}
pub(crate) async fn json<T: DeserializeOwned>(
request: RequestBuilder,
) -> Result<T, StorageError> {
let bytes = Self::send(request).await?.bytes().await?;
Ok(serde_json::from_slice(&bytes)?)
}
}
fn storage_base(project_url: &Url) -> Result<Url, StorageError> {
let base = project_url.join("storage/v1")?;
if base.cannot_be_a_base() {
return Err(StorageError::InvalidBaseUrl);
}
Ok(base)
}
fn sensitive(value: &str) -> Result<HeaderValue, StorageError> {
let mut header = HeaderValue::from_str(value)?;
header.set_sensitive(true);
Ok(header)
}