Skip to main content

lit/storage/
objects.rs

1use crate::core::{Object, ObjectHash};
2use crate::crypto::encryption::{EncryptionConfig, EncryptionManager};
3use flate2::read::ZlibDecoder;
4use flate2::write::ZlibEncoder;
5use flate2::Compression;
6use std::fs;
7use std::io::{Read, Write};
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, Mutex};
10
11/// Object storage - handles reading/writing objects to disk with optional encryption
12pub struct ObjectStore {
13    objects_dir: PathBuf,
14    /// Where `lit gc` puts packs. Objects live loose until they are packed, and
15    /// packing removes the loose copy, so every lookup that misses on disk has
16    /// to consult the packs before concluding the object is absent.
17    packs_dir: PathBuf,
18    encryption: Arc<Mutex<EncryptionManager>>,
19}
20
21impl ObjectStore {
22    /// Create a new object store
23    pub fn new(repo_path: &Path) -> Self {
24        let objects_dir = repo_path.join(".lit").join("objects");
25
26        // Load encryption configuration
27        let encryption_config = EncryptionConfig::load(repo_path).unwrap_or_default();
28        let encryption = Arc::new(Mutex::new(EncryptionManager::new_auto(
29            encryption_config,
30            repo_path,
31        )));
32
33        ObjectStore {
34            objects_dir,
35            packs_dir: crate::storage::pack::packs_dir(repo_path),
36            encryption,
37        }
38    }
39
40    /// Create object store with encryption passphrase
41    pub fn new_with_encryption(repo_path: &Path, passphrase: Option<&str>) -> Result<Self, String> {
42        let objects_dir = repo_path.join(".lit").join("objects");
43
44        // Load encryption configuration
45        let encryption_config = EncryptionConfig::load(repo_path)?;
46        let mut encryption_manager = EncryptionManager::new(encryption_config);
47
48        // Initialize encryption if passphrase provided
49        if let Some(pass) = passphrase {
50            encryption_manager.initialize(pass)?;
51        }
52
53        let encryption = Arc::new(Mutex::new(encryption_manager));
54
55        Ok(ObjectStore {
56            objects_dir,
57            packs_dir: crate::storage::pack::packs_dir(repo_path),
58            encryption,
59        })
60    }
61
62    /// Get the path for an object by its hash
63    /// Uses first 4 chars for directory sharding (65,536 shards for better distribution)
64    fn object_path(&self, hash: &ObjectHash) -> PathBuf {
65        let hash_str = hash.as_str();
66        let (dir, file) = hash_str.split_at(4);
67        self.objects_dir.join(dir).join(file)
68    }
69
70    /// Write an object to storage
71    pub fn write(&self, object: &Object) -> Result<ObjectHash, String> {
72        let hash = object.hash();
73        let path = self.object_path(&hash);
74
75        // Create parent directory
76        if let Some(parent) = path.parent() {
77            fs::create_dir_all(parent)
78                .map_err(|e| format!("Failed to create object directory: {}", e))?;
79        }
80
81        // Serialize and compress
82        let data = object.to_bytes();
83        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
84        encoder
85            .write_all(&data)
86            .map_err(|e| format!("Failed to compress object: {}", e))?;
87        let compressed = encoder
88            .finish()
89            .map_err(|e| format!("Failed to finish compression: {}", e))?;
90
91        // Encrypt if enabled
92        let final_data = {
93            let encryption = self
94                .encryption
95                .lock()
96                .map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
97            encryption.encrypt(&compressed)?
98        };
99
100        fs::write(&path, final_data).map_err(|e| format!("Failed to write object: {}", e))?;
101
102        Ok(hash)
103    }
104
105    /// Read an object from storage
106    pub fn read(&self, hash: &ObjectHash) -> Result<Object, String> {
107        let path = self.object_path(hash);
108
109        if !path.exists() {
110            // Not loose: it may have been packed by `lit gc`, which removes the
111            // loose copy once the pack is written.
112            return self.read_packed(hash);
113        }
114
115        // Read encrypted/compressed data
116        let encrypted_data =
117            fs::read(&path).map_err(|e| format!("Failed to read object: {}", e))?;
118
119        // Decrypt if enabled
120        let compressed = {
121            let encryption = self
122                .encryption
123                .lock()
124                .map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
125            encryption.decrypt(&encrypted_data)?
126        };
127
128        // Decompress
129        let mut decoder = ZlibDecoder::new(&compressed[..]);
130        let mut data = Vec::new();
131        decoder
132            .read_to_end(&mut data)
133            .map_err(|e| format!("Failed to decompress object: {}", e))?;
134
135        Object::from_bytes(&data)
136    }
137
138    /// Check if an object exists
139    pub fn exists(&self, hash: &ObjectHash) -> bool {
140        self.object_path(hash).exists()
141            || crate::storage::pack::load_all(&self.packs_dir).contains_key(hash.as_str())
142    }
143
144    /// Read an object that lives in a pack rather than loose on disk.
145    fn read_packed(&self, hash: &ObjectHash) -> Result<Object, String> {
146        let packed = crate::storage::pack::load_all(&self.packs_dir);
147        let Some((pack_path, offset)) = packed.get(hash.as_str()) else {
148            return Err(format!("Object {} not found", hash.short()));
149        };
150
151        let encryption = self
152            .encryption
153            .lock()
154            .map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
155
156        crate::storage::pack::read_pack_object(pack_path, *offset, &encryption)
157            .map_err(|e| format!("Failed to read {} from pack: {}", hash.short(), e))
158    }
159
160    /// The encryption manager this store reads and writes through.
161    ///
162    /// `gc` needs it so that packed objects get exactly the treatment the loose
163    /// ones had, rather than landing on disk in the clear.
164    pub fn encryption(&self) -> Arc<Mutex<EncryptionManager>> {
165        Arc::clone(&self.encryption)
166    }
167
168    /// List all objects
169    pub fn list(&self) -> Result<Vec<ObjectHash>, String> {
170        let mut objects = Vec::new();
171
172        // Packed objects have no loose file to walk, so take them from the pack
173        // indexes. A hash can appear in both if a pack was written but the loose
174        // copy not yet removed, so the two sets are de-duplicated at the end.
175        for hash in crate::storage::pack::load_all(&self.packs_dir).into_keys() {
176            objects.push(ObjectHash::from_hex(hash));
177        }
178
179        if !self.objects_dir.exists() {
180            return Ok(objects);
181        }
182
183        for entry in walkdir::WalkDir::new(&self.objects_dir)
184            .min_depth(2)
185            .max_depth(2)
186        {
187            let entry = entry.map_err(|e| format!("Failed to read objects: {}", e))?;
188
189            if entry.file_type().is_file() {
190                let path = entry.path();
191
192                // Reconstruct hash from path
193                if let Some(file_name) = path.file_name() {
194                    if let Some(parent) = path.parent() {
195                        if let Some(dir_name) = parent.file_name() {
196                            let hash = format!(
197                                "{}{}",
198                                dir_name.to_string_lossy(),
199                                file_name.to_string_lossy()
200                            );
201                            objects.push(ObjectHash::from_hex(hash));
202                        }
203                    }
204                }
205            }
206        }
207
208        objects.sort_by(|a, b| a.as_str().cmp(b.as_str()));
209        objects.dedup_by(|a, b| a.as_str() == b.as_str());
210        Ok(objects)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::core::Blob;
218    use tempfile::TempDir;
219
220    #[test]
221    fn test_object_store() {
222        let temp_dir = TempDir::new().unwrap();
223        let repo_path = temp_dir.path();
224
225        // Create .lit/objects directory
226        fs::create_dir_all(repo_path.join(".lit").join("objects")).unwrap();
227
228        let store = ObjectStore::new(repo_path);
229
230        // Create and write a blob
231        let content = b"Hello, world!".to_vec();
232        let blob = Blob::new(content.clone());
233        let object = Object::Blob(blob);
234
235        let hash = store.write(&object).unwrap();
236
237        // Read it back
238        let read_object = store.read(&hash).unwrap();
239
240        match read_object {
241            Object::Blob(blob) => assert_eq!(blob.content, content),
242            _ => panic!("Expected blob"),
243        }
244    }
245}