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}