pacsea/index/
persist.rs

1use std::fs;
2use std::path::Path;
3
4use super::{OfficialIndex, idx};
5
6/// What: Load the official index from `path` if a valid JSON exists.
7///
8/// Inputs:
9/// - `path`: File path to read JSON from
10///
11/// Output:
12/// - Replaces the in-memory index on success; ignores errors and leaves it unchanged on failure.
13///
14/// Details:
15/// - Silently ignores IO or deserialization failures to keep startup resilient.
16/// - Rebuilds the `name_to_idx` `HashMap` after deserialization for O(1) lookups.
17pub fn load_from_disk(path: &Path) {
18    if let Ok(s) = fs::read_to_string(path)
19        && let Ok(mut new_idx) = serde_json::from_str::<OfficialIndex>(&s)
20        && let Ok(mut guard) = idx().write()
21    {
22        // Rebuild the name index HashMap after deserialization
23        new_idx.rebuild_name_index();
24        *guard = new_idx;
25    }
26}
27
28/// What: Persist the current official index to `path` as JSON.
29///
30/// Inputs:
31/// - `path`: File path to write JSON to
32///
33/// Output:
34/// - Writes JSON to disk; errors are logged but not propagated to avoid interrupting the UI.
35///
36/// Details:
37/// - Serializes under a read lock and ensures parent directory exists before writing.
38/// - Creates parent directory if it doesn't exist (Windows-compatible).
39/// - Logs write failures for debugging but doesn't crash background tasks.
40/// - Warns if the index is empty when saving.
41pub fn save_to_disk(path: &Path) {
42    if let Ok(guard) = idx().read()
43        && let Ok(s) = serde_json::to_string(&*guard)
44    {
45        // Warn if index is empty
46        if guard.pkgs.is_empty() {
47            tracing::warn!(
48                path = %path.display(),
49                "Attempting to save empty index to disk"
50            );
51        }
52        // Ensure parent directory exists before writing
53        if let Some(parent) = path.parent()
54            && let Err(e) = fs::create_dir_all(parent)
55        {
56            tracing::warn!(
57                path = %path.display(),
58                error = %e,
59                "Failed to create parent directory for index file"
60            );
61            return;
62        }
63        // Write the file and log errors
64        if let Err(e) = fs::write(path, s) {
65            tracing::warn!(
66                path = %path.display(),
67                error = %e,
68                "Failed to write index file to disk"
69            );
70        } else {
71            tracing::info!(
72                path = %path.display(),
73                package_count = guard.pkgs.len(),
74                "Successfully saved index to disk"
75            );
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82
83    #[tokio::test]
84    /// What: Load multiple index snapshots and ensure deduplication.
85    ///
86    /// Inputs:
87    /// - Two JSON snapshots with overlapping package names written sequentially.
88    ///
89    /// Output:
90    /// - `all_official()` yields the unique names `aa` and `zz`.
91    ///
92    /// Details:
93    /// - Validates that reloading replaces the index without duplicating entries.
94    async fn index_loads_deduped_and_sorted_after_multiple_writes() {
95        use std::path::PathBuf;
96
97        let mut path: PathBuf = std::env::temp_dir();
98        path.push(format!(
99            "pacsea_idx_multi_{}_{}.json",
100            std::process::id(),
101            std::time::SystemTime::now()
102                .duration_since(std::time::UNIX_EPOCH)
103                .expect("System time is before UNIX epoch")
104                .as_nanos()
105        ));
106
107        let idx_json1 = serde_json::json!({
108            "pkgs": [
109                {"name": "zz", "repo": "extra", "arch": "x86_64", "version": "1", "description": ""},
110                {"name": "aa", "repo": "core", "arch": "x86_64", "version": "1", "description": ""}
111            ]
112        });
113        std::fs::write(
114            &path,
115            serde_json::to_string(&idx_json1).expect("failed to serialize index JSON"),
116        )
117        .expect("failed to write index JSON file");
118        super::load_from_disk(&path);
119
120        let idx_json2 = serde_json::json!({
121            "pkgs": [
122                {"name": "aa", "repo": "core", "arch": "x86_64", "version": "2", "description": ""},
123                {"name": "zz", "repo": "extra", "arch": "x86_64", "version": "1", "description": ""}
124            ]
125        });
126        std::fs::write(
127            &path,
128            serde_json::to_string(&idx_json2).expect("failed to serialize index JSON"),
129        )
130        .expect("failed to write index JSON file");
131        super::load_from_disk(&path);
132
133        let all = crate::index::all_official();
134        let mut names: Vec<String> = all.into_iter().map(|p| p.name).collect();
135        names.sort();
136        names.dedup();
137        assert_eq!(names, vec!["aa", "zz"]);
138
139        let _ = std::fs::remove_file(&path);
140    }
141
142    #[tokio::test]
143    /// What: Persist the in-memory index and confirm the file reflects current data.
144    ///
145    /// Inputs:
146    /// - Seed `idx()` with a single package prior to saving.
147    ///
148    /// Output:
149    /// - JSON file containing the seeded package name.
150    ///
151    /// Details:
152    /// - Uses a temp file cleaned up at the end to avoid polluting the workspace.
153    async fn index_save_writes_current_state_to_disk() {
154        use std::path::PathBuf;
155        // Prepare in-memory index
156        if let Ok(mut g) = super::idx().write() {
157            g.pkgs = vec![crate::index::OfficialPkg {
158                name: "abc".to_string(),
159                repo: "core".to_string(),
160                arch: "x86_64".to_string(),
161                version: "9".to_string(),
162                description: "desc".to_string(),
163            }];
164        }
165        // Temp path
166        let mut path: PathBuf = std::env::temp_dir();
167        path.push(format!(
168            "pacsea_idx_save_{}_{}.json",
169            std::process::id(),
170            std::time::SystemTime::now()
171                .duration_since(std::time::UNIX_EPOCH)
172                .expect("System time is before UNIX epoch")
173                .as_nanos()
174        ));
175        super::save_to_disk(&path);
176        // Read back and assert content contains our package name
177        let body = std::fs::read_to_string(&path).expect("failed to read index JSON file");
178        assert!(body.contains("\"abc\""));
179        let _ = std::fs::remove_file(&path);
180    }
181}