Skip to main content

ph_registry/
storage.rs

1use crate::error::{RegistryError, Result};
2use std::future::Future;
3use std::path::PathBuf;
4
5/// Abstraction over where layer files are stored.
6///
7/// Uses Rust 1.75+ RPITIT (`impl Future`) — no `async-trait` crate needed.
8/// Note: RPITIT traits are NOT object-safe (`Box<dyn Storage>` / `Arc<dyn Storage>` are
9/// rejected by the compiler). `AppState` uses `Arc<StorageBackend>` where `StorageBackend`
10/// is a concrete enum that dispatches to the correct backend. This avoids dynamic dispatch
11/// while keeping the trait clean for test use with `FilesystemStorage` directly.
12pub trait Storage: Send + Sync {
13    fn put(&self, key: &str, data: Vec<u8>) -> impl Future<Output = Result<()>> + Send;
14    fn get(&self, key: &str) -> impl Future<Output = Result<Vec<u8>>> + Send;
15    fn exists(&self, key: &str) -> impl Future<Output = Result<bool>> + Send;
16}
17
18/// Concrete enum that dispatches to the chosen storage backend.
19/// Used in `AppState` to avoid `dyn Storage` (RPITIT is not object-safe).
20pub enum StorageBackend {
21    Filesystem(FilesystemStorage),
22    // S3(S3Storage) will be added in a future iteration
23}
24
25impl StorageBackend {
26    pub async fn put(&self, key: &str, data: Vec<u8>) -> Result<()> {
27        match self {
28            StorageBackend::Filesystem(s) => s.put(key, data).await,
29        }
30    }
31    pub async fn get(&self, key: &str) -> Result<Vec<u8>> {
32        match self {
33            StorageBackend::Filesystem(s) => s.get(key).await,
34        }
35    }
36    pub async fn exists(&self, key: &str) -> Result<bool> {
37        match self {
38            StorageBackend::Filesystem(s) => s.exists(key).await,
39        }
40    }
41}
42
43/// Local filesystem storage backend (development / small teams).
44pub struct FilesystemStorage {
45    base: PathBuf,
46}
47
48impl FilesystemStorage {
49    pub fn new(base: impl Into<PathBuf>) -> Self {
50        Self { base: base.into() }
51    }
52}
53
54impl Storage for FilesystemStorage {
55    async fn put(&self, key: &str, data: Vec<u8>) -> Result<()> {
56        let path = self.base.join(key);
57        if let Some(parent) = path.parent() {
58            tokio::fs::create_dir_all(parent).await
59                .map_err(|e| RegistryError::Storage(e.to_string()))?;
60        }
61        tokio::fs::write(&path, data).await
62            .map_err(|e| RegistryError::Storage(e.to_string()))
63    }
64
65    async fn get(&self, key: &str) -> Result<Vec<u8>> {
66        let path = self.base.join(key);
67        tokio::fs::read(&path).await
68            .map_err(|_| RegistryError::NotFound(key.to_string()))
69    }
70
71    async fn exists(&self, key: &str) -> Result<bool> {
72        // Use tokio::fs::try_exists — avoids blocking the async executor
73        tokio::fs::try_exists(self.base.join(key)).await
74            .map_err(|e| RegistryError::Storage(e.to_string()))
75    }
76}
77
78/// Build the S3-style object key for a layer file.
79/// e.g. layer_key("base", "expert", "v1.0", "layer.yaml")
80///    → "layers/base/expert/v1.0/layer.yaml"
81pub fn layer_key(namespace: &str, name: &str, version: &str, filename: &str) -> String {
82    format!("layers/{}/{}/{}/{}", namespace, name, version, filename)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use tempfile::TempDir;
89
90    #[tokio::test]
91    async fn test_filesystem_put_and_get() {
92        let dir = TempDir::new().unwrap();
93        let storage = FilesystemStorage::new(dir.path());
94        storage.put("layers/base/expert/v1.0/layer.yaml", b"name: expert".to_vec()).await.unwrap();
95        let data = storage.get("layers/base/expert/v1.0/layer.yaml").await.unwrap();
96        assert_eq!(data, b"name: expert");
97    }
98
99    #[tokio::test]
100    async fn test_filesystem_get_missing_returns_not_found() {
101        let dir = TempDir::new().unwrap();
102        let storage = FilesystemStorage::new(dir.path());
103        let result = storage.get("nonexistent/key").await;
104        assert!(matches!(result, Err(RegistryError::NotFound(_))));
105    }
106
107    #[tokio::test]
108    async fn test_filesystem_exists() {
109        let dir = TempDir::new().unwrap();
110        let storage = FilesystemStorage::new(dir.path());
111        assert!(!storage.exists("foo/bar").await.unwrap());
112        storage.put("foo/bar", b"data".to_vec()).await.unwrap();
113        assert!(storage.exists("foo/bar").await.unwrap());
114    }
115
116    #[test]
117    fn test_layer_key_format() {
118        assert_eq!(
119            layer_key("base", "expert", "v1.0", "layer.yaml"),
120            "layers/base/expert/v1.0/layer.yaml"
121        );
122    }
123}