use std::sync::Arc;
use bytes::Bytes;
use object_store::path::Path;
use object_store::{GetResult, ObjectStore, ObjectStoreExt, PutPayload, WriteMultipart};
use crate::error::AdminError;
const MULTIPART_THRESHOLD: usize = 5 * 1024 * 1024;
const PART_CONCURRENCY: usize = 4;
#[derive(Clone, Debug)]
pub struct Storage {
store: Arc<dyn ObjectStore>,
prefix: Path,
}
impl Storage {
pub fn from_url<I>(url: &str, options: I) -> Result<Self, AdminError>
where
I: IntoIterator<Item = (String, String)>,
{
let parsed = url::Url::parse(url)
.map_err(|e| AdminError::Config(format!("invalid storage URL: {e}")))?;
let (store, prefix) = object_store::parse_url_opts(&parsed, options).map_err(|e| {
AdminError::Config(format!(
"cannot open storage `{}` (is the matching Cargo feature enabled?): {e}",
parsed.scheme()
))
})?;
Ok(Self {
store: Arc::from(store),
prefix,
})
}
pub fn from_env() -> Result<Option<Self>, AdminError> {
match std::env::var("STORAGE_URL") {
Ok(url) => Self::from_url(&url, std::env::vars()).map(Some),
Err(_) => Ok(None),
}
}
#[must_use]
pub fn new(store: Arc<dyn ObjectStore>, prefix: &str) -> Self {
Self {
store,
prefix: Path::from(prefix),
}
}
#[must_use]
pub fn memory() -> Self {
Self::new(Arc::new(object_store::memory::InMemory::new()), "")
}
fn path(&self, key: &str) -> Path {
key.split('/')
.filter(|part| !part.is_empty())
.fold(self.prefix.clone(), |path, part| path.join(part))
}
pub async fn put(&self, key: &str, bytes: Bytes) -> Result<(), AdminError> {
self.store
.put(&self.path(key), PutPayload::from(bytes))
.await?;
Ok(())
}
pub async fn get(&self, key: &str) -> Result<GetResult, AdminError> {
match self.store.get(&self.path(key)).await {
Ok(result) => Ok(result),
Err(object_store::Error::NotFound { .. }) => {
Err(AdminError::NotFound("stored object".into()))
}
Err(other) => Err(other.into()),
}
}
pub async fn delete(&self, key: &str) -> Result<(), AdminError> {
match self.store.delete(&self.path(key)).await {
Ok(()) | Err(object_store::Error::NotFound { .. }) => Ok(()),
Err(other) => Err(other.into()),
}
}
#[must_use]
pub fn sink(&self, key: &str) -> Sink {
Sink {
storage: self.clone(),
key: key.to_owned(),
buffer: Vec::new(),
multipart: None,
written: 0,
}
}
}
pub struct Sink {
storage: Storage,
key: String,
buffer: Vec<u8>,
multipart: Option<WriteMultipart>,
written: u64,
}
impl Sink {
#[must_use]
pub fn written(&self) -> u64 {
self.written
}
pub async fn write(&mut self, bytes: &[u8]) -> Result<(), AdminError> {
self.written += bytes.len() as u64;
if self.multipart.is_none() && self.buffer.len() + bytes.len() <= MULTIPART_THRESHOLD {
self.buffer.extend_from_slice(bytes);
return Ok(());
}
if self.multipart.is_none() {
let upload = self
.storage
.store
.put_multipart(&self.storage.path(&self.key))
.await?;
let mut writer = WriteMultipart::new_with_chunk_size(upload, MULTIPART_THRESHOLD);
writer.write(&std::mem::take(&mut self.buffer));
self.multipart = Some(writer);
}
if let Some(writer) = &mut self.multipart {
writer.wait_for_capacity(PART_CONCURRENCY).await?;
writer.write(bytes);
}
Ok(())
}
pub async fn finish(self) -> Result<u64, AdminError> {
match self.multipart {
Some(writer) => {
writer.finish().await?;
}
None => {
self.storage
.put(&self.key, Bytes::from(self.buffer))
.await?;
}
}
Ok(self.written)
}
pub async fn abort(self) {
if let Some(writer) = self.multipart
&& let Err(error) = writer.abort().await
{
tracing::warn!(%error, "could not abort multipart upload");
}
}
}