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>;
}
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")
);
}
}