Skip to main content

chkpt_core/index/
mod.rs

1use crate::error::Result;
2use bitcode::{Decode, Encode};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Encode, Decode)]
7pub struct FileEntry {
8    pub path: String,
9    pub blob_hash: [u8; 16],
10    pub size: u64,
11    pub mtime_secs: i64,
12    pub mtime_nanos: i64,
13    pub inode: Option<u64>,
14    pub mode: u32,
15}
16
17pub struct FileIndex {
18    path: PathBuf,
19    entries: HashMap<String, FileEntry>,
20}
21
22impl FileIndex {
23    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
24        let path = path.as_ref().to_path_buf();
25        let entries = match std::fs::read(&path) {
26            Ok(data) => match bitcode::decode::<Vec<FileEntry>>(&data) {
27                Ok(entries_vec) => {
28                    let mut entries = HashMap::with_capacity(entries_vec.len());
29                    for entry in entries_vec {
30                        entries.insert(entry.path.clone(), entry);
31                    }
32                    entries
33                }
34                Err(_) => {
35                    // Decode failure means the on-disk index was written by an
36                    // incompatible version (e.g. 32-byte hashes vs. 16-byte).
37                    // The index is a pure performance cache: treat it as empty
38                    // so the next save falls back to a full scan rather than
39                    // hard-failing.  The stale file will be overwritten on the
40                    // next successful flush.
41                    HashMap::new()
42                }
43            },
44            Err(error) if error.kind() == std::io::ErrorKind::NotFound => HashMap::new(),
45            Err(error) => return Err(error.into()),
46        };
47        Ok(Self { path, entries })
48    }
49
50    fn flush(&self) -> Result<()> {
51        let mut entries_vec = Vec::with_capacity(self.entries.len());
52        entries_vec.extend(self.entries.values().cloned());
53        let encoded = bitcode::encode(&entries_vec);
54        let tmp_path = self.path.with_extension("bin.tmp");
55        std::fs::write(&tmp_path, &encoded)?;
56        std::fs::rename(&tmp_path, &self.path)?;
57        Ok(())
58    }
59
60    pub fn upsert(&mut self, entry: &FileEntry) -> Result<()> {
61        self.entries.insert(entry.path.clone(), entry.clone());
62        self.flush()
63    }
64
65    pub fn bulk_upsert(&mut self, entries: &[FileEntry]) -> Result<()> {
66        self.apply_changes(&[], entries)
67    }
68
69    pub fn bulk_remove(&mut self, paths: &[String]) -> Result<()> {
70        self.apply_changes(paths, &[])
71    }
72
73    pub fn apply_changes(
74        &mut self,
75        paths_to_remove: &[String],
76        entries_to_upsert: &[FileEntry],
77    ) -> Result<()> {
78        if paths_to_remove.is_empty() && entries_to_upsert.is_empty() {
79            return Ok(());
80        }
81        for path in paths_to_remove {
82            self.entries.remove(path);
83        }
84        for entry in entries_to_upsert {
85            self.entries.insert(entry.path.clone(), entry.clone());
86        }
87        self.flush()
88    }
89
90    pub fn get(&self, path: &str) -> Result<Option<FileEntry>> {
91        Ok(self.entries.get(path).cloned())
92    }
93
94    pub fn remove(&mut self, path: &str) -> Result<()> {
95        self.entries.remove(path);
96        self.flush()
97    }
98
99    pub fn all_paths(&self) -> Result<Vec<String>> {
100        Ok(self.entries.keys().cloned().collect())
101    }
102
103    pub fn all_entries(&self) -> Result<Vec<FileEntry>> {
104        Ok(self.entries.values().cloned().collect())
105    }
106
107    pub fn entries(&self) -> &HashMap<String, FileEntry> {
108        &self.entries
109    }
110
111    pub fn entries_by_path(&self) -> Result<HashMap<String, FileEntry>> {
112        Ok(self.entries.clone())
113    }
114
115    pub fn clear(&mut self) -> Result<()> {
116        self.entries.clear();
117        self.flush()
118    }
119}