freighter-storage 1.0.1

Cloudflare's third-party Rust registry implementation
Documentation
use async_trait::async_trait;
use freighter_api_types::storage::{
    Bytes, Metadata, MetadataStorageProvider, StorageError, StorageResult,
};
use std::io;
use std::io::{Seek, Write};
use std::path::PathBuf;
use tempfile::NamedTempFile;

pub struct FsStorageProvider {
    root: PathBuf,
}

impl FsStorageProvider {
    pub fn new(root: PathBuf) -> StorageResult<Self> {
        std::fs::create_dir_all(&root)?;
        Ok(Self { root })
    }

    fn abs_path(&self, path: &str) -> StorageResult<PathBuf> {
        let path = self.root.join(path);
        if !path.starts_with(&self.root) {
            return Err(StorageError::ServiceError(
                io::Error::from(io::ErrorKind::InvalidInput).into(),
            ));
        }
        Ok(path)
    }
}

#[async_trait]
impl MetadataStorageProvider for FsStorageProvider {
    async fn pull_file(&self, path: &str) -> StorageResult<Bytes> {
        let data = std::fs::read(self.root.join(path))?;
        Ok(data.into())
    }

    async fn put_file(&self, path: &str, file_bytes: Bytes, _meta: Metadata) -> StorageResult<()> {
        let path = self.abs_path(path)?;
        let parent = path.parent().unwrap();
        if !parent.exists() {
            std::fs::create_dir_all(parent)?;
        }
        let mut tmp = NamedTempFile::new_in(parent)?;
        tmp.write_all(&file_bytes)?;
        tmp.persist(path).map_err(|e| StorageError::ServiceError(e.into()))?;
        Ok(())
    }

    async fn create_or_append_file(&self, path: &str, file_bytes: Bytes, _meta: Metadata) -> StorageResult<()> {
        let path = self.abs_path(path)?;
        let parent = path.parent().unwrap();
        if !parent.exists() {
            std::fs::create_dir_all(parent)?;
        }
        let mut file = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
        file.seek(io::SeekFrom::End(0))?;
        file.write_all(&file_bytes)?;
        Ok(())
    }

    async fn delete_file(&self, path: &str) -> StorageResult<()> {
        let path = self.abs_path(path)?;
        std::fs::remove_file(path)?;
        Ok(())
    }

    async fn healthcheck(&self) -> anyhow::Result<()> {
        if self.root.is_dir() {
            Ok(())
        } else {
            anyhow::bail!("root not a dir")
        }
    }
}