sui_dockerfile_node_cache_daemon/
store.rs1use std::collections::BTreeMap;
7use std::path::{Path, PathBuf};
8use std::sync::Mutex;
9
10use async_trait::async_trait;
11
12use crate::protocol::CachedArtifact;
13use crate::DaemonError;
14
15#[async_trait]
20pub trait LocalCacheStore: Send + Sync {
21 async fn get(&self, content_hash: &str) -> Result<Option<CachedArtifact>, DaemonError>;
22 async fn put(&self, content_hash: &str, artifact: &CachedArtifact) -> Result<(), DaemonError>;
23}
24
25pub struct RealLocalCacheStore {
34 root: PathBuf,
35}
36
37impl RealLocalCacheStore {
38 #[must_use]
39 pub fn new(root: PathBuf) -> Self {
40 Self { root }
41 }
42
43 fn entry_path(&self, content_hash: &str) -> PathBuf {
44 let shard = if content_hash.len() >= 2 { &content_hash[..2] } else { "00" };
45 self.root.join(shard).join(format!("{content_hash}.json"))
46 }
47}
48
49#[async_trait]
50impl LocalCacheStore for RealLocalCacheStore {
51 async fn get(&self, content_hash: &str) -> Result<Option<CachedArtifact>, DaemonError> {
52 let path = self.entry_path(content_hash);
53 match tokio::fs::read(&path).await {
54 Ok(bytes) => {
55 let artifact: CachedArtifact =
56 serde_json::from_slice(&bytes).map_err(|e| DaemonError::Protocol(e.to_string()))?;
57 Ok(Some(artifact))
58 }
59 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
60 Err(e) => Err(DaemonError::Io(e)),
61 }
62 }
63
64 async fn put(&self, content_hash: &str, artifact: &CachedArtifact) -> Result<(), DaemonError> {
65 let path = self.entry_path(content_hash);
66 if let Some(parent) = path.parent() {
67 tokio::fs::create_dir_all(parent).await?;
68 }
69 let body = serde_json::to_vec(artifact).map_err(|e| DaemonError::Protocol(e.to_string()))?;
70 let tmp_path = path.with_extension("json.tmp");
75 tokio::fs::write(&tmp_path, &body).await?;
76 tokio::fs::rename(&tmp_path, &path).await?;
77 Ok(())
78 }
79}
80
81#[derive(Default)]
83pub struct MockLocalCacheStore {
84 entries: Mutex<BTreeMap<String, CachedArtifact>>,
85 pub get_calls: Mutex<usize>,
88}
89
90impl MockLocalCacheStore {
91 #[must_use]
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 #[must_use]
101 pub fn with_entry(self, content_hash: &str, artifact: CachedArtifact) -> Self {
102 self.entries.lock().expect("mock mutex poisoned").insert(content_hash.to_string(), artifact);
103 self
104 }
105
106 #[must_use]
114 pub fn contains(&self, content_hash: &str) -> bool {
115 self.entries.lock().expect("mock mutex poisoned").contains_key(content_hash)
116 }
117}
118
119#[async_trait]
120impl LocalCacheStore for MockLocalCacheStore {
121 async fn get(&self, content_hash: &str) -> Result<Option<CachedArtifact>, DaemonError> {
122 *self.get_calls.lock().expect("mock mutex poisoned") += 1;
123 Ok(self.entries.lock().expect("mock mutex poisoned").get(content_hash).cloned())
124 }
125
126 async fn put(&self, content_hash: &str, artifact: &CachedArtifact) -> Result<(), DaemonError> {
127 self.entries
128 .lock()
129 .expect("mock mutex poisoned")
130 .insert(content_hash.to_string(), artifact.clone());
131 Ok(())
132 }
133}
134
135pub async fn ensure_root_exists(root: &Path) -> Result<(), DaemonError> {
144 tokio::fs::create_dir_all(root).await?;
145 Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[tokio::test]
153 async fn real_store_put_then_get_round_trips() {
154 let dir = tempfile::tempdir().unwrap();
155 let store = RealLocalCacheStore::new(dir.path().to_path_buf());
156 let artifact = CachedArtifact { image_ref: "example/image:v1".to_string() };
157 store.put("deadbeef", &artifact).await.unwrap();
158 let got = store.get("deadbeef").await.unwrap();
159 assert_eq!(got, Some(artifact));
160 }
161
162 #[tokio::test]
163 async fn real_store_miss_returns_none_not_an_error() {
164 let dir = tempfile::tempdir().unwrap();
165 let store = RealLocalCacheStore::new(dir.path().to_path_buf());
166 let got = store.get("nope").await.unwrap();
167 assert_eq!(got, None);
168 }
169
170 #[tokio::test]
171 async fn mock_store_get_calls_are_counted() {
172 let store = MockLocalCacheStore::new();
173 store.get("x").await.unwrap();
174 store.get("x").await.unwrap();
175 assert_eq!(*store.get_calls.lock().unwrap(), 2);
176 }
177}