2.4 KiB · text History 07a12d4
pub mod local;

use async_trait::async_trait;
use std::path::PathBuf;

pub use local::LocalStorage;

#[derive(Debug, thiserror::Error)]
pub enum StorageError {
    #[error("io: {0}")]
    Io(#[from] std::io::Error),
}

/// Where in the backend a file lives. For local storage, this is the path
/// relative to STORAGE_ROOT — workers and the file server can resolve it
/// against their own root.
#[derive(Debug, Clone)]
pub struct StoredObject {
    pub relative_path: String,
    pub size_bytes: u64,
    pub sha256_hex: String,
}

/// Where `put_derived_as` will place an artifact. Kept next to the trait so
/// readers can work out a derived path without going through storage — the
/// categorizers need the poster's path before anything has written it.
pub fn derived_relative(original_relative: &str, suffix: &str, extension: &str) -> String {
    let original = std::path::Path::new(original_relative);
    let stem = original
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("derived");
    let parent = original.parent().unwrap_or_else(|| std::path::Path::new(""));
    parent
        .join(format!("{stem}.{suffix}.{extension}"))
        .to_string_lossy()
        .into_owned()
}

#[async_trait]
pub trait Storage: Send + Sync {
    /// Persist `bytes` and return where it landed. Implementations should be
    /// content-addressed so identical bytes don't get duplicated.
    async fn put(&self, bytes: &[u8], extension: Option<&str>) -> Result<StoredObject, StorageError>;

    /// Resolve a relative path back to an absolute path on disk. Used by
    /// workers that need to read the original bytes (image processing, OCR).
    fn absolute_path(&self, relative: &str) -> PathBuf;

    /// Persist a derived artifact (thumbnail, etc.) at a deterministic
    /// sibling path, keeping the original's extension. Returns the relative
    /// path used.
    async fn put_derived(
        &self,
        original_relative: &str,
        suffix: &str,
        bytes: &[u8],
    ) -> Result<String, StorageError>;

    /// Same, but with an explicit extension. A poster frame lifted out of an
    /// `.mp4` is a JPEG, and naming it `.poster.mp4` would have the file
    /// server hand it back as `video/mp4`.
    async fn put_derived_as(
        &self,
        original_relative: &str,
        suffix: &str,
        extension: &str,
        bytes: &[u8],
    ) -> Result<String, StorageError>;
}