1use crate::error::{RegistryError, Result};
2use std::future::Future;
3use std::path::PathBuf;
4
5pub 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
18pub enum StorageBackend {
21 Filesystem(FilesystemStorage),
22 }
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
43pub 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 tokio::fs::try_exists(self.base.join(key)).await
74 .map_err(|e| RegistryError::Storage(e.to_string()))
75 }
76}
77
78pub 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}