pacsea/logic/files/
mod.rs

1//! File list resolution and diff computation for preflight checks.
2
3mod backup;
4mod db_sync;
5mod lists;
6mod pkgbuild_cache;
7mod pkgbuild_fetch;
8mod pkgbuild_parse;
9mod resolution;
10
11pub use backup::{get_backup_files, get_backup_files_from_installed};
12pub use db_sync::{
13    ensure_file_db_synced, get_file_db_sync_info, get_file_db_sync_timestamp, is_file_db_stale,
14};
15pub use lists::{get_installed_file_list, get_remote_file_list};
16pub use pkgbuild_cache::{PkgbuildSourceKind, flush_pkgbuild_cache, parse_pkgbuild_cached};
17pub use pkgbuild_fetch::{fetch_pkgbuild_sync, fetch_srcinfo_sync, get_pkgbuild_from_cache};
18pub use pkgbuild_parse::{
19    parse_backup_array_content, parse_backup_from_pkgbuild, parse_backup_from_srcinfo,
20    parse_install_paths_from_pkgbuild,
21};
22pub use resolution::{
23    batch_get_remote_file_lists, resolve_install_files, resolve_install_files_with_remote_list,
24    resolve_package_files, resolve_remove_files,
25};
26
27use crate::state::modal::PackageFileInfo;
28use crate::state::types::PackageItem;
29
30/// What: Determine file-level changes for a set of packages under a specific preflight action.
31///
32/// Inputs:
33/// - `items`: Package descriptors under consideration.
34/// - `action`: Preflight action (install or remove) influencing the comparison strategy.
35///
36/// Output:
37/// - Returns a vector of `PackageFileInfo` entries describing per-package file deltas.
38///
39/// Details:
40/// - Invokes pacman commands to compare remote and installed file lists while preserving package order.
41#[allow(clippy::missing_const_for_fn)]
42pub fn resolve_file_changes(
43    items: &[PackageItem],
44    action: crate::state::modal::PreflightAction,
45) -> Vec<PackageFileInfo> {
46    // Check if file database is stale, but don't force sync (let user decide)
47    // Only sync if database doesn't exist or is very old (>30 days)
48    const MAX_AUTO_SYNC_AGE_DAYS: u64 = 30;
49    let _span = tracing::info_span!(
50        "resolve_file_changes",
51        stage = "files",
52        item_count = items.len()
53    )
54    .entered();
55    let start_time = std::time::Instant::now();
56
57    if items.is_empty() {
58        tracing::warn!("No packages provided for file resolution");
59        return Vec::new();
60    }
61    match ensure_file_db_synced(false, MAX_AUTO_SYNC_AGE_DAYS) {
62        Ok(synced) => {
63            if synced {
64                tracing::info!("File database was synced automatically (was very stale)");
65            } else {
66                tracing::debug!("File database is fresh, no sync needed");
67            }
68        }
69        Err(e) => {
70            // Sync failed (likely requires root), but continue anyway
71            tracing::warn!("File database sync failed: {} (continuing without sync)", e);
72        }
73    }
74
75    // Batch fetch remote file lists for all official packages to reduce pacman command overhead
76    let official_packages: Vec<(&str, &crate::state::types::Source)> = items
77        .iter()
78        .filter_map(|item| {
79            if matches!(item.source, crate::state::types::Source::Official { .. }) {
80                Some((item.name.as_str(), &item.source))
81            } else {
82                None
83            }
84        })
85        .collect();
86    let batched_remote_files_cache = if !official_packages.is_empty()
87        && matches!(action, crate::state::modal::PreflightAction::Install)
88    {
89        resolution::batch_get_remote_file_lists(&official_packages)
90    } else {
91        std::collections::HashMap::new()
92    };
93
94    let mut results = Vec::new();
95
96    for (idx, item) in items.iter().enumerate() {
97        tracing::info!(
98            "[{}/{}] Resolving files for package: {} ({:?})",
99            idx + 1,
100            items.len(),
101            item.name,
102            item.source
103        );
104
105        // Check if we have batched results for this official package
106        let use_batched = matches!(action, crate::state::modal::PreflightAction::Install)
107            && matches!(item.source, crate::state::types::Source::Official { .. })
108            && batched_remote_files_cache.contains_key(item.name.as_str());
109
110        match if use_batched {
111            // Use batched file list
112            let remote_files = batched_remote_files_cache
113                .get(item.name.as_str())
114                .cloned()
115                .unwrap_or_default();
116            resolution::resolve_install_files_with_remote_list(
117                &item.name,
118                &item.source,
119                remote_files,
120            )
121        } else {
122            resolution::resolve_package_files(&item.name, &item.source, action)
123        } {
124            Ok(file_info) => {
125                tracing::info!(
126                    "  Found {} files for {} ({} new, {} changed, {} removed)",
127                    file_info.total_count,
128                    item.name,
129                    file_info.new_count,
130                    file_info.changed_count,
131                    file_info.removed_count
132                );
133                results.push(file_info);
134            }
135            Err(e) => {
136                tracing::warn!("  Failed to resolve files for {}: {}", item.name, e);
137                // Create empty entry to maintain package order
138                results.push(PackageFileInfo {
139                    name: item.name.clone(),
140                    files: Vec::new(),
141                    total_count: 0,
142                    new_count: 0,
143                    changed_count: 0,
144                    removed_count: 0,
145                    config_count: 0,
146                    pacnew_candidates: 0,
147                    pacsave_candidates: 0,
148                });
149            }
150        }
151    }
152
153    let elapsed = start_time.elapsed();
154    let duration_ms = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX);
155    tracing::info!(
156        stage = "files",
157        item_count = items.len(),
158        result_count = results.len(),
159        duration_ms = duration_ms,
160        "File resolution complete"
161    );
162    results
163}
164
165#[cfg(all(test, unix))]
166mod tests;