heddle_pack/store/pack/
manager.rs1use std::{
5 collections::HashMap,
6 fs,
7 path::{Path, PathBuf},
8};
9
10use tracing::{debug, instrument, trace};
11
12use crate::{
13 object::ContentHash,
14 store::{
15 Result,
16 pack::{ObjectType, PackObjectId, PackReader},
17 },
18};
19
20pub struct PackManager {
24 packs_dir: PathBuf,
25 packs: Vec<CachedPack>,
26 object_locations: HashMap<PackObjectId, usize>,
27}
28
29struct CachedPack {
30 pack_path: PathBuf,
31 index_path: PathBuf,
32 reader: PackReader<'static>,
33}
34
35impl PackManager {
36 pub fn new(packs_dir: PathBuf) -> Self {
37 let packs = Self::load_packs(&packs_dir).unwrap_or_default();
38 let object_locations = Self::index_object_locations(&packs);
39 Self {
40 packs_dir,
41 packs,
42 object_locations,
43 }
44 }
45
46 fn discover_pack_paths(packs_dir: &Path) -> Result<Vec<(PathBuf, PathBuf)>> {
47 let mut packs = Vec::new();
48
49 if !packs_dir.exists() {
50 return Ok(packs);
51 }
52
53 for entry in fs::read_dir(packs_dir)? {
54 let entry = entry?;
55 let path = entry.path();
56
57 if path.extension().map(|e| e == "pack").unwrap_or(false) {
58 let index_path = path.with_extension("idx");
59 if index_path.exists() {
60 packs.push((path, index_path));
61 }
62 }
63 }
64
65 packs.sort_by(|left, right| left.0.cmp(&right.0));
66
67 debug!(count = packs.len(), "Discovered pack files");
68 Ok(packs)
69 }
70
71 fn load_packs(packs_dir: &Path) -> Result<Vec<CachedPack>> {
72 let mut cached_packs = Vec::new();
73
74 for (pack_path, index_path) in Self::discover_pack_paths(packs_dir)? {
75 match PackReader::open(&pack_path, &index_path) {
76 Ok(reader) => cached_packs.push(CachedPack {
77 pack_path,
78 index_path,
79 reader,
80 }),
81 Err(error) => {
82 debug!("Failed to open pack {:?}: {}", pack_path, error);
83 }
84 }
85 }
86
87 Ok(cached_packs)
88 }
89
90 pub fn reload(&mut self) -> Result<()> {
91 let packs = Self::load_packs(&self.packs_dir)?;
92 let object_locations = Self::index_object_locations(&packs);
93 self.packs = packs;
94 self.object_locations = object_locations;
95 Ok(())
96 }
97
98 fn index_object_locations(packs: &[CachedPack]) -> HashMap<PackObjectId, usize> {
99 let mut locations = HashMap::new();
100 for (pack_index, pack) in packs.iter().enumerate() {
101 for id in pack.reader.list_ids() {
102 locations.entry(id).or_insert(pack_index);
105 }
106 }
107 locations
108 }
109
110 pub fn add_pack(&mut self, pack_path: PathBuf, index_path: PathBuf) -> Result<()> {
112 if self.packs.iter().any(|pack| pack.pack_path == pack_path) {
113 return Ok(());
114 }
115 let cached = CachedPack {
116 reader: PackReader::open(&pack_path, &index_path)?,
117 pack_path,
118 index_path,
119 };
120 let pack_index = self.packs.len();
121 for id in cached.reader.list_ids() {
122 self.object_locations.entry(id).or_insert(pack_index);
123 }
124 self.packs.push(cached);
125 Ok(())
126 }
127
128 pub fn needs_reload(&self) -> Result<bool> {
134 Ok(Self::discover_pack_paths(&self.packs_dir)?.len() > self.packs.len())
135 }
136
137 pub fn reload_if_disk_grew(&mut self) -> Result<bool> {
148 if !self.needs_reload()? {
149 return Ok(false);
150 }
151 debug!("PackManager: pack dir grew under us, reloading");
152 self.reload()?;
153 Ok(true)
154 }
155
156 pub fn get_object(&self, id: &PackObjectId) -> Result<Option<(ObjectType, Vec<u8>)>> {
157 let Some(pack_index) = self.object_locations.get(id).copied() else {
158 trace!("Object not found in any pack");
159 return Ok(None);
160 };
161 let object = self.packs[pack_index].reader.get_object(id)?;
162 if object.is_some() {
163 trace!("Found object in pack");
164 }
165 Ok(object)
166 }
167
168 #[instrument(skip(self), fields(hash = %hash.short()))]
169 pub fn get_hashed_object(&self, hash: &ContentHash) -> Result<Option<(ObjectType, Vec<u8>)>> {
170 self.get_object(&PackObjectId::Hash(*hash))
171 }
172
173 pub fn get_hashed_object_bytes(
178 &self,
179 hash: &ContentHash,
180 ) -> Result<Option<(ObjectType, bytes::Bytes)>> {
181 let id = PackObjectId::Hash(*hash);
182 let Some(pack_index) = self.object_locations.get(&id).copied() else {
183 return Ok(None);
184 };
185 self.packs[pack_index].reader.get_object_bytes(&id)
186 }
187
188 pub fn has_object(&self, hash: &ContentHash) -> bool {
189 self.object_locations
190 .contains_key(&PackObjectId::Hash(*hash))
191 }
192
193 pub fn get_hashed_object_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
197 let id = PackObjectId::Hash(*hash);
198 let Some(pack_index) = self.object_locations.get(&id).copied() else {
199 return Ok(None);
200 };
201 self.packs[pack_index].reader.get_hashed_object_size(hash)
202 }
203
204 pub fn has_object_id(&self, id: &PackObjectId) -> bool {
205 self.object_locations.contains_key(id)
206 }
207
208 pub fn list_all_hashes(&self) -> Result<Vec<ContentHash>> {
210 let mut hashes = Vec::new();
211 for pack in &self.packs {
212 hashes.extend(pack.reader.list_hashes());
213 }
214 Ok(hashes)
215 }
216
217 pub fn list_all_ids(&self) -> Result<Vec<PackObjectId>> {
218 let mut ids = Vec::new();
219 for pack in &self.packs {
220 ids.extend(pack.reader.list_ids());
221 }
222 Ok(ids)
223 }
224
225 pub fn pack_file_paths(&self) -> Vec<(&Path, &Path)> {
227 self.packs
228 .iter()
229 .map(|pack| (pack.pack_path.as_path(), pack.index_path.as_path()))
230 .collect()
231 }
232
233 pub fn pack_count(&self) -> usize {
234 self.packs.len()
235 }
236
237 pub fn packs_dir(&self) -> &Path {
238 &self.packs_dir
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use heddle_format::compression::CompressionConfig;
245 use tempfile::TempDir;
246
247 use super::PackManager;
248 use crate::{
249 object::ContentHash,
250 store::pack::{ObjectType, PackBuilder},
251 };
252
253 fn write_pack(
254 root: &std::path::Path,
255 ordinal: usize,
256 ) -> (std::path::PathBuf, std::path::PathBuf, ContentHash) {
257 let payload = format!("pack-object-{ordinal}").into_bytes();
258 let object_id = ContentHash::compute(&payload);
259 let mut builder = PackBuilder::new(CompressionConfig {
260 max_delta_size: 0,
261 ..CompressionConfig::default()
262 });
263 builder.add(object_id, ObjectType::Blob, payload);
264 let (pack_data, index_data, _) = builder.build().unwrap();
265 let pack_path = root.join(format!("format-{ordinal:03}.pack"));
266 let index_path = root.join(format!("format-{ordinal:03}.idx"));
267 std::fs::write(&pack_path, pack_data).unwrap();
268 std::fs::write(&index_path, index_data).unwrap();
269 (pack_path, index_path, object_id)
270 }
271
272 #[test]
273 fn object_locator_tracks_incremental_and_reloaded_packs() {
274 let temp = TempDir::new().unwrap();
275 let mut manager = PackManager::new(temp.path().to_path_buf());
276 for ordinal in 0..8 {
277 let (pack_path, index_path, _) = write_pack(temp.path(), ordinal);
278 manager.add_pack(pack_path, index_path).unwrap();
279 }
280
281 let ids = manager.list_all_ids().unwrap();
282 assert_eq!(ids.len(), 8);
283 for id in &ids {
284 assert!(manager.has_object_id(id));
285 assert!(manager.get_object(id).unwrap().is_some());
286 }
287
288 let reloaded = PackManager::new(temp.path().to_path_buf());
289 assert_eq!(reloaded.pack_count(), 8);
290 for id in &ids {
291 assert!(reloaded.has_object_id(id));
292 assert!(reloaded.get_object(id).unwrap().is_some());
293 }
294 }
295}