firecloud_storage/
manifest.rs1use crate::{StorageError, StorageResult};
9use firecloud_core::{FileId, FileManifest};
10#[cfg(test)]
11use firecloud_core::{ChunkHash, FileMetadata};
12use sled::Db;
13use std::path::Path;
14use tracing::{debug, info, warn};
15
16const TREE_MANIFESTS: &str = "manifests";
18const TREE_FILE_INDEX: &str = "file_index"; pub struct ManifestStore {
22 db: Db,
23}
24
25impl ManifestStore {
26 pub fn open<P: AsRef<Path>>(path: P) -> StorageResult<Self> {
28 let db = sled::open(path).map_err(|e| StorageError::Database(e.to_string()))?;
29 info!("Opened manifest store with Sled");
30 Ok(Self { db })
31 }
32
33 pub fn put(&self, manifest: &FileManifest) -> StorageResult<()> {
35 let manifests_tree = self
36 .db
37 .open_tree(TREE_MANIFESTS)
38 .map_err(|e| StorageError::Database(e.to_string()))?;
39 let index_tree = self
40 .db
41 .open_tree(TREE_FILE_INDEX)
42 .map_err(|e| StorageError::Database(e.to_string()))?;
43
44 let key = manifest.metadata.id.0.as_bytes();
46
47 let manifest_bytes =
49 bincode::serialize(manifest).map_err(|e| StorageError::Serialization(e.to_string()))?;
50 manifests_tree
51 .insert(key, manifest_bytes)
52 .map_err(|e| StorageError::Database(e.to_string()))?;
53
54 let name_key = manifest.metadata.name.as_bytes();
56 index_tree
57 .insert(name_key, key)
58 .map_err(|e| StorageError::Database(e.to_string()))?;
59
60 debug!(
61 "Stored manifest: {} ({})",
62 manifest.metadata.id, manifest.metadata.name
63 );
64 Ok(())
65 }
66
67 pub fn get(&self, file_id: &FileId) -> StorageResult<Option<FileManifest>> {
69 let manifests_tree = self
70 .db
71 .open_tree(TREE_MANIFESTS)
72 .map_err(|e| StorageError::Database(e.to_string()))?;
73
74 let key = file_id.0.as_bytes();
75
76 match manifests_tree
77 .get(key)
78 .map_err(|e| StorageError::Database(e.to_string()))?
79 {
80 Some(bytes) => {
81 let manifest: FileManifest = bincode::deserialize(&bytes)
82 .map_err(|e| StorageError::Serialization(e.to_string()))?;
83 Ok(Some(manifest))
84 }
85 None => Ok(None),
86 }
87 }
88
89 pub fn get_by_id_str(&self, file_id_str: &str) -> StorageResult<Option<FileManifest>> {
91 let uuid = uuid::Uuid::parse_str(file_id_str)
92 .map_err(|e| StorageError::InvalidId(format!("Invalid file ID: {}", e)))?;
93 let file_id = FileId::from_uuid(uuid);
94 self.get(&file_id)
95 }
96
97 pub fn get_by_name(&self, name: &str) -> StorageResult<Option<FileManifest>> {
99 let index_tree = self
100 .db
101 .open_tree(TREE_FILE_INDEX)
102 .map_err(|e| StorageError::Database(e.to_string()))?;
103
104 let name_key = name.as_bytes();
105
106 match index_tree
107 .get(name_key)
108 .map_err(|e| StorageError::Database(e.to_string()))?
109 {
110 Some(file_id_bytes) => {
111 let uuid = uuid::Uuid::from_slice(&file_id_bytes)
113 .map_err(|e| StorageError::Serialization(e.to_string()))?;
114 let file_id = FileId::from_uuid(uuid);
115 self.get(&file_id)
116 }
117 None => Ok(None),
118 }
119 }
120
121 pub fn contains(&self, file_id: &FileId) -> StorageResult<bool> {
123 let manifests_tree = self
124 .db
125 .open_tree(TREE_MANIFESTS)
126 .map_err(|e| StorageError::Database(e.to_string()))?;
127 Ok(manifests_tree
128 .contains_key(file_id.0.as_bytes())
129 .map_err(|e| StorageError::Database(e.to_string()))?)
130 }
131
132 pub fn delete(&self, file_id: &FileId) -> StorageResult<bool> {
134 let manifests_tree = self
135 .db
136 .open_tree(TREE_MANIFESTS)
137 .map_err(|e| StorageError::Database(e.to_string()))?;
138 let index_tree = self
139 .db
140 .open_tree(TREE_FILE_INDEX)
141 .map_err(|e| StorageError::Database(e.to_string()))?;
142
143 let key = file_id.0.as_bytes();
145 if let Some(bytes) = manifests_tree
146 .get(key)
147 .map_err(|e| StorageError::Database(e.to_string()))?
148 {
149 let manifest: FileManifest = bincode::deserialize(&bytes)
150 .map_err(|e| StorageError::Serialization(e.to_string()))?;
151
152 index_tree
154 .remove(manifest.metadata.name.as_bytes())
155 .map_err(|e| StorageError::Database(e.to_string()))?;
156 }
157
158 let existed = manifests_tree
160 .remove(key)
161 .map_err(|e| StorageError::Database(e.to_string()))?
162 .is_some();
163
164 if existed {
165 debug!("Deleted manifest: {}", file_id);
166 }
167 Ok(existed)
168 }
169
170 pub fn list(&self) -> StorageResult<Vec<ManifestSummary>> {
172 let manifests_tree = self
173 .db
174 .open_tree(TREE_MANIFESTS)
175 .map_err(|e| StorageError::Database(e.to_string()))?;
176
177 let mut summaries = Vec::new();
178
179 for result in manifests_tree.iter() {
180 let (_, value) = result.map_err(|e| StorageError::Database(e.to_string()))?;
181
182 match bincode::deserialize::<FileManifest>(&value) {
184 Ok(manifest) => {
185 summaries.push(ManifestSummary {
186 file_id: manifest.metadata.id,
187 name: manifest.metadata.name.clone(),
188 size: manifest.metadata.size,
189 chunk_count: manifest.chunks.len(),
190 created_at: manifest.metadata.created_at,
191 });
192 }
193 Err(e) => {
194 warn!("Failed to deserialize manifest (skipping): {}", e);
196 continue;
197 }
198 }
199 }
200
201 Ok(summaries)
202 }
203
204 pub fn count(&self) -> StorageResult<usize> {
206 let manifests_tree = self
207 .db
208 .open_tree(TREE_MANIFESTS)
209 .map_err(|e| StorageError::Database(e.to_string()))?;
210 Ok(manifests_tree.len())
211 }
212
213 pub fn flush(&self) -> StorageResult<()> {
215 self.db
216 .flush()
217 .map_err(|e| StorageError::Database(e.to_string()))?;
218 Ok(())
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct ManifestSummary {
225 pub file_id: FileId,
226 pub name: String,
227 pub size: u64,
228 pub chunk_count: usize,
229 pub created_at: i64,
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use tempfile::TempDir;
236
237 fn create_test_manifest() -> FileManifest {
238 let metadata = FileMetadata {
239 id: FileId::new(),
240 name: "test_file.txt".to_string(),
241 mime_type: Some("text/plain".to_string()),
242 size: 1024,
243 created_at: chrono::Utc::now().timestamp_millis(),
244 modified_at: chrono::Utc::now().timestamp_millis(),
245 content_hash: ChunkHash::hash(b"test content"),
246 };
247
248 FileManifest {
249 metadata,
250 chunks: vec![
251 ChunkHash::hash(b"chunk1"),
252 ChunkHash::hash(b"chunk2"),
253 ],
254 encrypted_dek: Some(vec![0u8; 32]),
255 salt: Some(vec![0u8; 16]), version: 1,
257 }
258 }
259
260 #[test]
261 fn test_manifest_store_put_get() {
262 let tmp_dir = TempDir::new().unwrap();
263 let store = ManifestStore::open(tmp_dir.path().join("manifests")).unwrap();
264
265 let manifest = create_test_manifest();
266 let file_id = manifest.metadata.id;
267
268 store.put(&manifest).unwrap();
270
271 let retrieved = store.get(&file_id).unwrap().unwrap();
273 assert_eq!(retrieved.metadata.name, "test_file.txt");
274 assert_eq!(retrieved.chunks.len(), 2);
275 }
276
277 #[test]
278 fn test_manifest_store_get_by_name() {
279 let tmp_dir = TempDir::new().unwrap();
280 let store = ManifestStore::open(tmp_dir.path().join("manifests")).unwrap();
281
282 let manifest = create_test_manifest();
283 store.put(&manifest).unwrap();
284
285 let retrieved = store.get_by_name("test_file.txt").unwrap().unwrap();
287 assert_eq!(retrieved.metadata.id, manifest.metadata.id);
288 }
289
290 #[test]
291 fn test_manifest_store_list() {
292 let tmp_dir = TempDir::new().unwrap();
293 let store = ManifestStore::open(tmp_dir.path().join("manifests")).unwrap();
294
295 for i in 0..3 {
297 let mut manifest = create_test_manifest();
298 manifest.metadata.name = format!("file_{}.txt", i);
299 store.put(&manifest).unwrap();
300 }
301
302 let summaries = store.list().unwrap();
303 assert_eq!(summaries.len(), 3);
304 }
305}