Skip to main content

pg_embedded_setup_unpriv/cache/operations/
lookup.rs

1//! Cache lookup and hit/miss detection.
2//!
3//! Provides functions for checking cache status and finding matching versions.
4
5use camino::{Utf8Path, Utf8PathBuf};
6use postgresql_embedded::{Version, VersionReq};
7use std::fs;
8use tracing::{debug, warn};
9
10use super::copy::copy_from_cache;
11
12/// Marker file name indicating a complete cache entry.
13pub(crate) const COMPLETION_MARKER: &str = ".complete";
14
15/// Observability target for cache operations.
16const LOG_TARGET: &str = "pg_embed::cache";
17
18/// Result of a cache lookup operation.
19#[derive(Debug)]
20#[must_use]
21pub enum CacheLookupResult {
22    /// Cache hit: binaries exist and are valid.
23    Hit {
24        /// Path to the cached version directory.
25        source_dir: Utf8PathBuf,
26    },
27    /// Cache miss: binaries need to be downloaded.
28    Miss,
29}
30
31/// Returns true if the cache entry at the given path is complete.
32///
33/// A cache entry is complete if both the `.complete` marker and `bin/`
34/// directory exist.
35fn is_cache_entry_complete(version_dir: &Utf8Path) -> bool {
36    let marker = version_dir.join(COMPLETION_MARKER);
37    let bin_dir = version_dir.join("bin");
38    marker.exists() && bin_dir.is_dir()
39}
40
41/// Checks if the cache contains valid binaries for the given version.
42///
43/// A cache entry is considered valid if:
44/// 1. The version directory exists
45/// 2. The `.complete` marker file is present
46/// 3. The `bin` subdirectory exists (indicating extracted binaries)
47///
48/// # Arguments
49///
50/// * `cache_dir` - Root directory of the binary cache
51/// * `version` - Exact version string to look up (e.g., "17.4.0")
52///
53/// # Examples
54///
55/// ```no_run
56/// use camino::Utf8Path;
57/// use pg_embedded_setup_unpriv::cache::{check_cache, CacheLookupResult};
58///
59/// let cache_dir = Utf8Path::new("/home/user/.cache/pg-embedded/binaries");
60/// match check_cache(cache_dir, "17.4.0") {
61///     CacheLookupResult::Hit { source_dir } => {
62///         println!("Found cached binaries at {source_dir}");
63///     }
64///     CacheLookupResult::Miss => {
65///         println!("Cache miss, need to download");
66///     }
67/// }
68/// ```
69pub fn check_cache(cache_dir: &Utf8Path, version: &str) -> CacheLookupResult {
70    let version_dir = cache_dir.join(version);
71
72    if is_cache_entry_complete(&version_dir) {
73        debug!(
74            target: LOG_TARGET,
75            version = %version,
76            path = %version_dir,
77            "cache hit"
78        );
79        CacheLookupResult::Hit {
80            source_dir: version_dir,
81        }
82    } else {
83        log_cache_miss(&version_dir, version);
84        CacheLookupResult::Miss
85    }
86}
87
88/// Logs details about a cache miss for debugging.
89fn log_cache_miss(version_dir: &Utf8Path, version: &str) {
90    let marker = version_dir.join(COMPLETION_MARKER);
91    let bin_dir = version_dir.join("bin");
92    debug!(
93        target: LOG_TARGET,
94        version = %version,
95        marker_exists = marker.exists(),
96        bin_exists = bin_dir.is_dir(),
97        "cache miss"
98    );
99}
100
101/// Finds a cached version that satisfies the given version requirement.
102///
103/// Scans the cache directory for version subdirectories and returns the highest
104/// version that matches the requirement. This allows a requirement like `^17` to
105/// use a cached `17.4.0` entry.
106///
107/// # Arguments
108///
109/// * `cache_dir` - Root directory of the binary cache
110/// * `version_req` - Version requirement to match against (e.g., `^17`, `=17.4.0`)
111///
112/// # Returns
113///
114/// Returns `Some((version_string, source_dir))` if a matching cache entry is found,
115/// `None` otherwise.
116///
117/// # Examples
118///
119/// ```no_run
120/// use camino::Utf8Path;
121/// use postgresql_embedded::VersionReq;
122/// use pg_embedded_setup_unpriv::cache::find_matching_cached_version;
123///
124/// let cache_dir = Utf8Path::new("/home/user/.cache/pg-embedded/binaries");
125/// let version_req = VersionReq::parse("^17").expect("valid version req");
126/// if let Some((version, source_dir)) = find_matching_cached_version(cache_dir, &version_req) {
127///     println!("Found cached {version} at {source_dir}");
128/// }
129/// ```
130#[must_use]
131pub fn find_matching_cached_version(
132    cache_dir: &Utf8Path,
133    version_req: &VersionReq,
134) -> Option<(String, Utf8PathBuf)> {
135    let dir_entries = read_cache_directory(cache_dir)?;
136
137    // Use max_by for O(n) instead of collect + sort for O(n log n)
138    let (version, path) = dir_entries
139        .filter_map(Result::ok)
140        .filter_map(|entry| try_parse_cache_entry(&entry, version_req))
141        .max_by(|a, b| a.0.cmp(&b.0))?;
142
143    let version_str = version.to_string();
144    debug!(
145        target: LOG_TARGET,
146        version_req = %version_req,
147        matched_version = %version_str,
148        path = %path,
149        "found matching cached version"
150    );
151    Some((version_str, path))
152}
153
154/// Reads the cache directory, logging errors as debug messages.
155fn read_cache_directory(cache_dir: &Utf8Path) -> Option<fs::ReadDir> {
156    match fs::read_dir(cache_dir) {
157        Ok(entries) => Some(entries),
158        Err(err) => {
159            debug!(
160                target: LOG_TARGET,
161                cache_dir = %cache_dir,
162                error = %err,
163                "failed to read cache directory"
164            );
165            None
166        }
167    }
168}
169
170/// Attempts to parse a directory entry as a valid cache entry matching the version requirement.
171fn try_parse_cache_entry(
172    entry: &fs::DirEntry,
173    version_req: &VersionReq,
174) -> Option<(Version, Utf8PathBuf)> {
175    let path = entry.path();
176    let dir_name = path.file_name()?.to_str()?;
177
178    // Skip hidden directories
179    if dir_name.starts_with('.') {
180        return None;
181    }
182
183    let version = Version::parse(dir_name).ok()?;
184    if !version_req.matches(&version) {
185        return None;
186    }
187
188    let utf8_path = Utf8PathBuf::from_path_buf(path).ok()?;
189    is_cache_entry_complete(&utf8_path).then_some((version, utf8_path))
190}
191
192/// Attempts to use the cache, falling back gracefully on errors.
193///
194/// This is a convenience wrapper that logs warnings instead of failing when
195/// cache operations encounter errors.
196///
197/// # Arguments
198///
199/// * `cache_dir` - Root directory of the binary cache
200/// * `version` - Version string to look up
201/// * `target` - Target installation directory for copy
202///
203/// # Returns
204///
205/// Returns `true` if binaries were successfully copied from cache, `false` if
206/// the cache was missed or an error occurred.
207#[must_use]
208pub fn try_use_cache(cache_dir: &Utf8Path, version: &str, target: &Utf8Path) -> bool {
209    let CacheLookupResult::Hit { source_dir } = check_cache(cache_dir, version) else {
210        return false;
211    };
212
213    match copy_from_cache(&source_dir, target) {
214        Ok(()) => true,
215        Err(err) => {
216            warn!(
217                target: LOG_TARGET,
218                error = %err,
219                version = %version,
220                "cache copy failed, falling back to download"
221            );
222            false
223        }
224    }
225}