use std::io;
use std::path::{Component, Path, PathBuf};
use std::pin::Pin;
use bytes::Bytes;
use futures::Stream;
use laterite_core::strata::async_trait;
pub type ByteStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("storage io error: {0}")]
Io(#[from] io::Error),
#[error("refused a path outside the disk")]
BadPath,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlobMeta {
pub size: u64,
}
#[async_trait]
pub trait StorageDriver: Send + Sync + 'static {
async fn put(&self, path: &str, data: ByteStream) -> Result<(), StorageError>;
async fn get(&self, path: &str) -> Result<ByteStream, StorageError>;
async fn delete(&self, path: &str) -> Result<(), StorageError>;
async fn metadata(&self, path: &str) -> Result<Option<BlobMeta>, StorageError>;
fn public_url(&self, path: &str) -> Option<String>;
async fn presigned_put(
&self,
path: &str,
content_type: &str,
expires_in: std::time::Duration,
) -> Result<Option<String>, StorageError> {
let _ = (path, content_type, expires_in);
Ok(None)
}
async fn copy(&self, from: &str, to: &str) -> Result<(), StorageError> {
let data = self.get(from).await?;
self.put(to, data).await
}
async fn signed_url(
&self,
path: &str,
expires_in: std::time::Duration,
disposition: Disposition,
) -> Result<Option<String>, StorageError> {
let _ = (path, expires_in, disposition);
Ok(None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Disposition {
Inline,
Attachment,
}
pub(crate) fn resolve(root: &Path, path: &str) -> Result<PathBuf, StorageError> {
let candidate = Path::new(path);
if candidate.is_absolute() {
return Err(StorageError::BadPath);
}
let mut out = root.to_path_buf();
for part in candidate.components() {
match part {
Component::Normal(segment) => out.push(segment),
_ => return Err(StorageError::BadPath),
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_path_may_not_escape_its_disk() {
let root = Path::new("/srv/media");
for bad in [
"../etc/passwd",
"blobs/../../etc/passwd",
"/etc/passwd",
"./blobs/ab/cd/hash",
] {
assert!(
matches!(resolve(root, bad), Err(StorageError::BadPath)),
"{bad:?} was allowed"
);
}
}
#[test]
fn an_ordinary_blob_path_resolves_under_the_root() {
let root = Path::new("/srv/media");
assert_eq!(
resolve(root, "blobs/ab/cd/abcdef").unwrap(),
Path::new("/srv/media/blobs/ab/cd/abcdef")
);
}
}