ant_node/upgrade/cache_dir.rs
1//! Shared cache directory for upgrade artifacts.
2//!
3//! Multiple ant-node instances on the same machine share a single cache
4//! directory so that release metadata and downloaded binaries are fetched only
5//! once, reducing GitHub API calls and bandwidth.
6
7use crate::error::{Error, Result};
8use std::fs;
9use std::path::PathBuf;
10
11/// Return the shared upgrade cache directory, creating it on demand.
12///
13/// The path is `{data_dir}/upgrades/` where `data_dir` comes from
14/// `directories::ProjectDirs` (e.g. `~/.local/share/ant/upgrades/` on
15/// Linux).
16///
17/// # Errors
18///
19/// Returns an error if the platform data directory cannot be determined or
20/// the directory cannot be created.
21pub fn upgrade_cache_dir() -> Result<PathBuf> {
22 let project_dirs = directories::ProjectDirs::from("", "", "ant").ok_or_else(|| {
23 Error::Upgrade("Cannot determine platform data directory for upgrade cache".to_string())
24 })?;
25
26 let cache_dir = project_dirs.data_dir().join("upgrades");
27 fs::create_dir_all(&cache_dir)?;
28
29 // Defence in depth: restrict the shared upgrade cache to the owning
30 // user (0700) so a co-located low-privilege process cannot
31 // write/tamper with cached archives in the first place. The ML-DSA
32 // re-verification on every cache hit is the primary control; this just
33 // shrinks the attack surface. Best-effort on Unix; a failure to tighten
34 // permissions must not break upgrades (the crypto gate still holds).
35 #[cfg(unix)]
36 {
37 use std::os::unix::fs::PermissionsExt;
38 if let Ok(meta) = fs::metadata(&cache_dir) {
39 let mut perms = meta.permissions();
40 perms.set_mode(0o700);
41 if let Err(e) = fs::set_permissions(&cache_dir, perms) {
42 crate::logging::warn!(
43 "Could not tighten upgrade cache dir permissions to 0700 ({e}); \
44 ML-DSA re-verification still protects cached archives"
45 );
46 }
47 }
48 }
49
50 Ok(cache_dir)
51}
52
53#[cfg(test)]
54#[allow(clippy::unwrap_used, clippy::expect_used)]
55mod tests {
56 use super::*;
57
58 /// Verify the function succeeds and returns a path ending in "upgrades".
59 ///
60 /// Note: this creates a real directory under the platform data dir.
61 /// Modifying env vars to isolate this requires `unsafe` (due to
62 /// `deny(unsafe_code)`), so we accept the minor side-effect.
63 #[test]
64 fn test_upgrade_cache_dir_returns_path() {
65 let dir = upgrade_cache_dir().unwrap();
66 assert!(dir.exists());
67 assert!(dir.ends_with("upgrades"));
68 }
69}