2.5 KiB · text History 1ce9641
use async_trait::async_trait;
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use tokio::fs;
use tokio::io::AsyncWriteExt;

use super::{Storage, StorageError, StoredObject};

#[derive(Clone)]
pub struct LocalStorage {
    root: PathBuf,
}

impl LocalStorage {
    pub fn new(root: impl Into<PathBuf>) -> Self {
        Self { root: root.into() }
    }

    fn relative_for(hash_hex: &str, extension: Option<&str>) -> String {
        // sha256/aa/bb/full-hash[.ext] — fan out to keep directory sizes sane
        let aa = &hash_hex[0..2];
        let bb = &hash_hex[2..4];
        match extension {
            Some(ext) if !ext.is_empty() => format!("sha256/{aa}/{bb}/{hash_hex}.{ext}"),
            _ => format!("sha256/{aa}/{bb}/{hash_hex}"),
        }
    }
}

#[async_trait]
impl Storage for LocalStorage {
    async fn put(&self, bytes: &[u8], extension: Option<&str>) -> Result<StoredObject, StorageError> {
        let mut hasher = Sha256::new();
        hasher.update(bytes);
        let hash_hex = hex::encode(hasher.finalize());
        let relative = Self::relative_for(&hash_hex, extension);
        let abs = self.root.join(&relative);

        if let Some(parent) = abs.parent() {
            fs::create_dir_all(parent).await?;
        }
        if !abs.exists() {
            let mut f = fs::File::create(&abs).await?;
            f.write_all(bytes).await?;
            f.sync_all().await?;
        }

        Ok(StoredObject {
            relative_path: relative,
            size_bytes: bytes.len() as u64,
            sha256_hex: hash_hex,
        })
    }

    fn absolute_path(&self, relative: &str) -> PathBuf {
        self.root.join(relative)
    }

    async fn put_derived(
        &self,
        original_relative: &str,
        suffix: &str,
        bytes: &[u8],
    ) -> Result<String, StorageError> {
        let original = Path::new(original_relative);
        let stem = original
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("derived");
        let ext = original.extension().and_then(|s| s.to_str()).unwrap_or("bin");
        let parent = original.parent().unwrap_or_else(|| Path::new(""));
        let derived_rel = parent
            .join(format!("{stem}.{suffix}.{ext}"))
            .to_string_lossy()
            .into_owned();

        let abs = self.root.join(&derived_rel);
        if let Some(p) = abs.parent() {
            fs::create_dir_all(p).await?;
        }
        let mut f = fs::File::create(&abs).await?;
        f.write_all(bytes).await?;
        f.sync_all().await?;
        Ok(derived_rel)
    }
}