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