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    /// Create an object store that uses a manager the caller already built.
63    ///
64    /// `rotate-key` needs to write objects under a key that is not the one the
65    /// key file describes yet, which no passphrase-based constructor can
66    /// express.
67    pub fn with_encryption_manager(repo_path: &Path, manager: EncryptionManager) -> Self {
68        ObjectStore {
69            objects_dir: repo_path.join(".lit").join("objects"),
70            packs_dir: crate::storage::pack::packs_dir(repo_path),
71            encryption: Arc::new(Mutex::new(manager)),
72        }
73    }
74
75    /// Get the path for an object by its hash
76    /// Uses first 4 chars for directory sharding (65,536 shards for better distribution)
77    fn object_path(&self, hash: &ObjectHash) -> PathBuf {
78        let hash_str = hash.as_str();
79        let (dir, file) = hash_str.split_at(4);
80        self.objects_dir.join(dir).join(file)
81    }
82
83    /// Write an object to storage
84    pub fn write(&self, object: &Object) -> Result<ObjectHash, String> {
85        let hash = object.hash();
86        let path = self.object_path(&hash);
87
88        // Create parent directory
89        if let Some(parent) = path.parent() {
90            fs::create_dir_all(parent)
91                .map_err(|e| format!("Failed to create object directory: {}", e))?;
92        }
93
94        // Serialize and compress
95        let data = object.to_bytes();
96        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
97        encoder
98            .write_all(&data)
99            .map_err(|e| format!("Failed to compress object: {}", e))?;
100        let compressed = encoder
101            .finish()
102            .map_err(|e| format!("Failed to finish compression: {}", e))?;
103
104        // Encrypt if enabled
105        let final_data = {
106            let encryption = self
107                .encryption
108                .lock()
109                .map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
110            encryption.encrypt(&compressed)?
111        };
112
113        fs::write(&path, final_data).map_err(|e| format!("Failed to write object: {}", e))?;
114
115        Ok(hash)
116    }
117
118    /// Read an object from storage
119    pub fn read(&self, hash: &ObjectHash) -> Result<Object, String> {
120        let path = self.object_path(hash);
121
122        if !path.exists() {
123            // Not loose: it may have been packed by `lit gc`, which removes the
124            // loose copy once the pack is written.
125            return self.read_packed(hash);
126        }
127
128        // Read encrypted/compressed data
129        let encrypted_data =
130            fs::read(&path).map_err(|e| format!("Failed to read object: {}", e))?;
131
132        // Decrypt if enabled
133        let compressed = {
134            let encryption = self
135                .encryption
136                .lock()
137                .map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
138            encryption.decrypt(&encrypted_data)?
139        };
140
141        // Decompress
142        let mut decoder = ZlibDecoder::new(&compressed[..]);
143        let mut data = Vec::new();
144        decoder
145            .read_to_end(&mut data)
146            .map_err(|e| format!("Failed to decompress object: {}", e))?;
147
148        Object::from_bytes(&data)
149    }
150
151    /// Check if an object exists
152    pub fn exists(&self, hash: &ObjectHash) -> bool {
153        self.object_path(hash).exists()
154            || crate::storage::pack::load_all(&self.packs_dir).contains_key(hash.as_str())
155    }
156
157    /// Read an object that lives in a pack rather than loose on disk.
158    fn read_packed(&self, hash: &ObjectHash) -> Result<Object, String> {
159        let packed = crate::storage::pack::load_all(&self.packs_dir);
160        let Some((pack_path, offset)) = packed.get(hash.as_str()) else {
161            return Err(format!("Object {} not found", hash.short()));
162        };
163
164        let encryption = self
165            .encryption
166            .lock()
167            .map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
168
169        crate::storage::pack::read_pack_object(pack_path, *offset, &encryption)
170            .map_err(|e| format!("Failed to read {} from pack: {}", hash.short(), e))
171    }
172
173    /// The encryption manager this store reads and writes through.
174    ///
175    /// `gc` needs it so that packed objects get exactly the treatment the loose
176    /// ones had, rather than landing on disk in the clear.
177    pub fn encryption(&self) -> Arc<Mutex<EncryptionManager>> {
178        Arc::clone(&self.encryption)
179    }
180
181    /// List all objects
182    pub fn list(&self) -> Result<Vec<ObjectHash>, String> {
183        let mut objects = Vec::new();
184
185        // Packed objects have no loose file to walk, so take them from the pack
186        // indexes. A hash can appear in both if a pack was written but the loose
187        // copy not yet removed, so the two sets are de-duplicated at the end.
188        for hash in crate::storage::pack::load_all(&self.packs_dir).into_keys() {
189            objects.push(ObjectHash::from_hex(hash));
190        }
191
192        if !self.objects_dir.exists() {
193            return Ok(objects);
194        }
195
196        for entry in walkdir::WalkDir::new(&self.objects_dir)
197            .min_depth(2)
198            .max_depth(2)
199        {
200            let entry = entry.map_err(|e| format!("Failed to read objects: {}", e))?;
201
202            if entry.file_type().is_file() {
203                let path = entry.path();
204
205                // Reconstruct hash from path
206                if let Some(file_name) = path.file_name() {
207                    if let Some(parent) = path.parent() {
208                        if let Some(dir_name) = parent.file_name() {
209                            let hash = format!(
210                                "{}{}",
211                                dir_name.to_string_lossy(),
212                                file_name.to_string_lossy()
213                            );
214                            objects.push(ObjectHash::from_hex(hash));
215                        }
216                    }
217                }
218            }
219        }
220
221        objects.sort_by(|a, b| a.as_str().cmp(b.as_str()));
222        objects.dedup_by(|a, b| a.as_str() == b.as_str());
223        Ok(objects)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::core::Blob;
231    use tempfile::TempDir;
232
233    #[test]
234    fn test_object_store() {
235        let temp_dir = TempDir::new().unwrap();
236        let repo_path = temp_dir.path();
237
238        // Create .lit/objects directory
239        fs::create_dir_all(repo_path.join(".lit").join("objects")).unwrap();
240
241        let store = ObjectStore::new(repo_path);
242
243        // Create and write a blob
244        let content = b"Hello, world!".to_vec();
245        let blob = Blob::new(content.clone());
246        let object = Object::Blob(blob);
247
248        let hash = store.write(&object).unwrap();
249
250        // Read it back
251        let read_object = store.read(&hash).unwrap();
252
253        match read_object {
254            Object::Blob(blob) => assert_eq!(blob.content, content),
255            _ => panic!("Expected blob"),
256        }
257    }
258}