pacsea/logic/files/db_sync.rs
1//! Database synchronization functions for pacman file database.
2
3use std::path::Path;
4use std::process::Command;
5use std::time::SystemTime;
6
7/// What: Retrieve the most recent modification timestamp of the pacman sync database.
8///
9/// Inputs:
10/// - (none): Reads metadata from `/var/lib/pacman/sync` on the local filesystem.
11///
12/// Output:
13/// - Returns the latest `SystemTime` seen among `.files` databases, or `None` if unavailable.
14///
15/// Details:
16/// - Inspects only files ending with the `.files` extension to match pacman's file list databases.
17pub fn get_file_db_sync_timestamp() -> Option<SystemTime> {
18 // Check modification time of pacman sync database files
19 // The sync database files are in /var/lib/pacman/sync/
20 let sync_dir = Path::new("/var/lib/pacman/sync");
21
22 if !sync_dir.exists() {
23 tracing::debug!("Pacman sync directory does not exist");
24 return None;
25 }
26
27 // Get the most recent modification time from any .files database
28 let mut latest_time: Option<SystemTime> = None;
29
30 if let Ok(entries) = std::fs::read_dir(sync_dir) {
31 for entry in entries.flatten() {
32 let path = entry.path();
33 // Look for .files database files (e.g., core.files, extra.files)
34 if path.extension().and_then(|s| s.to_str()) == Some("files")
35 && let Ok(metadata) = std::fs::metadata(&path)
36 && let Ok(modified) = metadata.modified()
37 {
38 latest_time = Some(latest_time.map_or(modified, |prev| {
39 if modified > prev { modified } else { prev }
40 }));
41 }
42 }
43 }
44
45 latest_time
46}
47
48/// What: Summarize sync database staleness with age, formatted date, and UI color bucket.
49///
50/// Inputs:
51/// - (none): Uses `get_file_db_sync_timestamp` to determine the last sync.
52///
53/// Output:
54/// - Returns `(age_days, formatted_date, color_category)` or `None` when the timestamp cannot be read.
55///
56/// Details:
57/// - Buckets age into three categories: green (<7 days), yellow (<30 days), red (>=30 days).
58#[must_use]
59pub fn get_file_db_sync_info() -> Option<(u64, String, u8)> {
60 let sync_time = get_file_db_sync_timestamp()?;
61
62 let now = SystemTime::now();
63 let age = now.duration_since(sync_time).ok()?;
64 let age_days = age.as_secs() / 86400; // Convert to days
65
66 // Format date
67 let date_str = crate::util::ts_to_date(
68 sync_time
69 .duration_since(SystemTime::UNIX_EPOCH)
70 .ok()
71 .and_then(|d| i64::try_from(d.as_secs()).ok()),
72 );
73
74 // Determine color category
75 let color_category = if age_days < 7 {
76 0 // Green (< week)
77 } else if age_days < 30 {
78 1 // Yellow (< month)
79 } else {
80 2 // Red (>= month)
81 };
82
83 Some((age_days, date_str, color_category))
84}
85
86/// What: Check if the pacman file database is stale and needs syncing.
87///
88/// Inputs:
89/// - `max_age_days`: Maximum age in days before considering the database stale.
90///
91/// Output:
92/// - Returns `Some(true)` if stale, `Some(false)` if fresh, `None` if timestamp cannot be determined.
93///
94/// Details:
95/// - Uses `get_file_db_sync_timestamp()` to check the last sync time.
96#[must_use]
97pub fn is_file_db_stale(max_age_days: u64) -> Option<bool> {
98 let sync_time = get_file_db_sync_timestamp()?;
99 let now = SystemTime::now();
100 let age = now.duration_since(sync_time).ok()?;
101 let age_days = age.as_secs() / 86400;
102 Some(age_days >= max_age_days)
103}
104
105/// What: Attempt a best-effort synchronization of the pacman file database.
106///
107/// Inputs:
108/// - `force`: If true, sync regardless of timestamp. If false, only sync if stale.
109/// - `max_age_days`: Maximum age in days before considering the database stale (default: 7).
110///
111/// Output:
112/// - Returns `Ok(true)` if sync was performed, `Ok(false)` if sync was skipped (fresh DB), `Err` if sync failed.
113///
114/// # Errors
115/// - Returns `Err` when `pacman -Fy` command execution fails (I/O error)
116/// - Returns `Err` when `pacman -Fy` exits with non-zero status
117///
118/// Details:
119/// - Checks timestamp first if `force` is false, only syncing when stale.
120/// - Intended to reduce false negatives when later querying remote file lists.
121pub fn ensure_file_db_synced(force: bool, max_age_days: u64) -> Result<bool, String> {
122 // Check if we need to sync
123 if force {
124 tracing::debug!("Force syncing pacman file database...");
125 } else if let Some(is_stale) = is_file_db_stale(max_age_days) {
126 if is_stale {
127 tracing::debug!(
128 "File database is stale (older than {} days), syncing...",
129 max_age_days
130 );
131 } else {
132 tracing::debug!("File database is fresh, skipping sync");
133 return Ok(false);
134 }
135 } else {
136 // Can't determine timestamp, try to sync anyway
137 tracing::debug!("Cannot determine file database timestamp, attempting sync...");
138 }
139
140 let output = Command::new("pacman")
141 .args(["-Fy"])
142 .env("LC_ALL", "C")
143 .env("LANG", "C")
144 .output()
145 .map_err(|e| format!("Failed to execute pacman -Fy: {e}"))?;
146
147 if output.status.success() {
148 tracing::debug!("File database sync successful");
149 Ok(true)
150 } else {
151 let stderr = String::from_utf8_lossy(&output.stderr);
152 let error_msg = format!("File database sync failed: {stderr}");
153 tracing::warn!("{}", error_msg);
154 Err(error_msg)
155 }
156}