entrenar/storage/cloud/
local.rs1use crate::storage::cloud::error::{CloudError, Result};
4use crate::storage::cloud::metadata::ArtifactMetadata;
5use crate::storage::cloud::traits::{compute_hash, ArtifactBackend};
6use std::collections::HashMap;
7use std::io::{Read, Write};
8use std::path::PathBuf;
9use std::sync::{Arc, RwLock};
10
11#[derive(Debug)]
13pub struct LocalBackend {
14 base_path: PathBuf,
15 metadata: Arc<RwLock<HashMap<String, ArtifactMetadata>>>,
16}
17
18impl LocalBackend {
19 pub fn new(base_path: PathBuf) -> Self {
21 Self { base_path, metadata: Arc::new(RwLock::new(HashMap::new())) }
22 }
23
24 pub fn new_and_init(base_path: PathBuf) -> Result<Self> {
26 std::fs::create_dir_all(&base_path)?;
27 Ok(Self::new(base_path))
28 }
29
30 fn hash_to_path(&self, hash: &str) -> PathBuf {
32 let prefix = hash.get(..2).unwrap_or(hash);
34 self.base_path.join(prefix).join(hash)
35 }
36}
37
38impl ArtifactBackend for LocalBackend {
39 fn put(&self, name: &str, data: &[u8]) -> Result<String> {
40 let hash = compute_hash(data);
41 let path = self.hash_to_path(&hash);
42
43 if let Some(parent) = path.parent() {
45 std::fs::create_dir_all(parent)?;
46 }
47
48 let mut file = std::fs::File::create(&path)?;
50 file.write_all(data)?;
51
52 let metadata = ArtifactMetadata::new(name, &hash, data.len() as u64);
54 self.metadata
55 .write()
56 .expect("metadata RwLock must not be poisoned")
57 .insert(hash.clone(), metadata);
58
59 Ok(hash)
60 }
61
62 fn get(&self, hash: &str) -> Result<Vec<u8>> {
63 let path = self.hash_to_path(hash);
64
65 if !path.exists() {
66 return Err(CloudError::NotFound(hash.to_string()));
67 }
68
69 let mut file = std::fs::File::open(&path)?;
70 let mut data = Vec::new();
71 file.read_to_end(&mut data)?;
72
73 let computed = compute_hash(&data);
75 if computed != hash {
76 return Err(CloudError::Backend(format!(
77 "Hash mismatch: expected {hash}, got {computed}"
78 )));
79 }
80
81 Ok(data)
82 }
83
84 fn exists(&self, hash: &str) -> Result<bool> {
85 let path = self.hash_to_path(hash);
86 Ok(path.exists())
87 }
88
89 fn delete(&self, hash: &str) -> Result<()> {
90 let path = self.hash_to_path(hash);
91
92 if !path.exists() {
93 return Err(CloudError::NotFound(hash.to_string()));
94 }
95
96 std::fs::remove_file(&path)?;
97 self.metadata.write().expect("metadata RwLock must not be poisoned").remove(hash);
98
99 Ok(())
100 }
101
102 fn get_metadata(&self, hash: &str) -> Result<ArtifactMetadata> {
103 self.metadata
104 .read()
105 .expect("metadata RwLock must not be poisoned")
106 .get(hash)
107 .cloned()
108 .ok_or_else(|| CloudError::NotFound(hash.to_string()))
109 }
110
111 fn list(&self) -> Result<Vec<ArtifactMetadata>> {
112 Ok(self
113 .metadata
114 .read()
115 .expect("metadata RwLock must not be poisoned")
116 .values()
117 .cloned()
118 .collect())
119 }
120
121 fn backend_type(&self) -> &'static str {
122 "local"
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use tempfile::TempDir;
130
131 #[test]
132 fn test_local_backend_put_get() {
133 let tmp = TempDir::new().expect("temp file creation should succeed");
134 let backend =
135 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
136
137 let data = b"local test data";
138 let hash = backend.put("test.bin", data).expect("operation should succeed");
139
140 let retrieved = backend.get(&hash).expect("key should exist");
141 assert_eq!(retrieved, data);
142 }
143
144 #[test]
145 fn test_local_backend_exists() {
146 let tmp = TempDir::new().expect("temp file creation should succeed");
147 let backend =
148 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
149
150 let hash = backend.put("test.bin", b"data").expect("operation should succeed");
151 assert!(backend.exists(&hash).expect("operation should succeed"));
152 assert!(!backend.exists("nonexistent").expect("operation should succeed"));
153 }
154
155 #[test]
156 fn test_local_backend_delete() {
157 let tmp = TempDir::new().expect("temp file creation should succeed");
158 let backend =
159 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
160
161 let hash = backend.put("test.bin", b"data").expect("operation should succeed");
162 backend.delete(&hash).expect("operation should succeed");
163 assert!(!backend.exists(&hash).expect("operation should succeed"));
164 }
165
166 #[test]
167 fn test_local_backend_type() {
168 let tmp = TempDir::new().expect("temp file creation should succeed");
169 let backend =
170 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
171 assert_eq!(backend.backend_type(), "local");
172 }
173
174 #[test]
175 fn test_local_backend_get_not_found() {
176 let tmp = TempDir::new().expect("temp file creation should succeed");
177 let backend =
178 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
179
180 let result = backend.get("nonexistent_hash");
181 assert!(result.is_err());
182 match result {
183 Err(CloudError::NotFound(hash)) => assert_eq!(hash, "nonexistent_hash"),
184 _ => panic!("Expected NotFound error"),
185 }
186 }
187
188 #[test]
189 fn test_local_backend_delete_not_found() {
190 let tmp = TempDir::new().expect("temp file creation should succeed");
191 let backend =
192 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
193
194 let result = backend.delete("nonexistent_hash");
195 assert!(result.is_err());
196 }
197
198 #[test]
199 fn test_local_backend_get_metadata() {
200 let tmp = TempDir::new().expect("temp file creation should succeed");
201 let backend =
202 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
203
204 let data = b"test data for metadata";
205 let hash = backend.put("test_file.bin", data).expect("operation should succeed");
206
207 let meta = backend.get_metadata(&hash).expect("operation should succeed");
208 assert_eq!(meta.name, "test_file.bin");
209 assert_eq!(meta.size, data.len() as u64);
210 }
211
212 #[test]
213 fn test_local_backend_get_metadata_not_found() {
214 let tmp = TempDir::new().expect("temp file creation should succeed");
215 let backend =
216 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
217
218 let result = backend.get_metadata("nonexistent");
219 assert!(result.is_err());
220 }
221
222 #[test]
223 fn test_local_backend_list() {
224 let tmp = TempDir::new().expect("temp file creation should succeed");
225 let backend =
226 LocalBackend::new_and_init(tmp.path().to_path_buf()).expect("operation should succeed");
227
228 backend.put("file1.bin", b"data1").expect("operation should succeed");
229 backend.put("file2.bin", b"data2").expect("operation should succeed");
230 backend.put("file3.bin", b"data3").expect("operation should succeed");
231
232 let list = backend.list().expect("operation should succeed");
233 assert_eq!(list.len(), 3);
234 }
235
236 #[test]
237 fn test_local_backend_new() {
238 let backend = LocalBackend::new(PathBuf::from("/tmp/test"));
239 assert_eq!(backend.backend_type(), "local");
240 }
241}