use std::path::{Path, PathBuf};
use futures::TryStreamExt;
use laterite_core::strata::async_trait;
use tokio::io::AsyncWriteExt;
use tokio_util::io::ReaderStream;
use crate::driver::{resolve, BlobMeta, ByteStream, StorageDriver, StorageError};
pub struct LocalDisk {
root: PathBuf,
url: Option<String>,
}
impl LocalDisk {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
url: None,
}
}
pub fn public_at(mut self, prefix: impl Into<String>) -> Self {
self.url = Some(prefix.into());
self
}
}
#[async_trait]
impl StorageDriver for LocalDisk {
async fn put(&self, path: &str, data: ByteStream) -> Result<(), StorageError> {
let target = resolve(&self.root, path)?;
if let Some(parent) = target.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let temp = target.with_extension(format!("part-{}", std::process::id()));
let mut file = tokio::fs::File::create(&temp).await?;
let mut data = data;
let write = async {
while let Some(chunk) = data.try_next().await? {
file.write_all(&chunk).await?;
}
file.flush().await?;
file.sync_all().await?;
Ok::<_, std::io::Error>(())
}
.await;
if let Err(err) = write {
let _ = tokio::fs::remove_file(&temp).await;
return Err(err.into());
}
tokio::fs::rename(&temp, &target).await?;
Ok(())
}
async fn get(&self, path: &str) -> Result<ByteStream, StorageError> {
let file = tokio::fs::File::open(resolve(&self.root, path)?).await?;
Ok(Box::pin(ReaderStream::new(file)))
}
async fn delete(&self, path: &str) -> Result<(), StorageError> {
match tokio::fs::remove_file(resolve(&self.root, path)?).await {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
}
async fn metadata(&self, path: &str) -> Result<Option<BlobMeta>, StorageError> {
match tokio::fs::metadata(resolve(&self.root, path)?).await {
Ok(m) => Ok(Some(BlobMeta { size: m.len() })),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
fn public_url(&self, path: &str) -> Option<String> {
let prefix = self.url.as_ref()?;
Some(format!(
"{}/{}",
prefix.trim_end_matches('/'),
path.trim_start_matches('/')
))
}
}
impl std::fmt::Debug for LocalDisk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LocalDisk")
.field("root", &self.root)
.field("public", &self.url.is_some())
.finish()
}
}
impl AsRef<Path> for LocalDisk {
fn as_ref(&self) -> &Path {
&self.root
}
}