tiger_pkg/manager/
path_cache.rs1use std::{
2 fs,
3 path::{Path, PathBuf},
4 time::SystemTime,
5};
6
7use ahash::HashMap;
8use itertools::Itertools;
9use tracing::info;
10
11use super::PackageManager;
12use crate::{package::PackagePlatform, GameVersion, Version};
13
14impl PackageManager {
15 #[cfg(feature = "ignore_package_cache")]
16 pub(super) fn read_package_cache(silent: bool) -> Option<PathCache> {
17 if !silent {
18 info!("Not loading tag cache: ignore_package_cache is enabled")
19 }
20 None
21 }
22
23 #[cfg(feature = "ignore_package_cache")]
24 pub(super) fn write_package_cache(&self) -> anyhow::Result<()> {
25 Ok(())
26 }
27
28 #[cfg(not(feature = "ignore_package_cache"))]
29 pub(super) fn read_package_cache(silent: bool) -> Option<PathCache> {
30 let cache: Option<PathCache> = serde_json::from_str(
31 &std::fs::read_to_string(exe_relative_path("package_cache.json")).ok()?,
32 )
33 .ok();
34
35 if let Some(ref c) = cache {
36 if c.cache_version != PathCache::VERSION {
37 if !silent {
38 tracing::warn!("Package cache is outdated, building a new one");
39 }
40 return None;
41 }
42 }
43
44 cache
45 }
46
47 #[cfg(not(feature = "ignore_package_cache"))]
48 pub(super) fn write_package_cache(&self) -> anyhow::Result<()> {
49 let mut cache = Self::read_package_cache(true).unwrap_or_default();
50
51 let timestamp = fs::metadata(&self.package_dir)
52 .ok()
53 .and_then(|m| {
54 Some(
55 m.modified()
56 .ok()?
57 .duration_since(SystemTime::UNIX_EPOCH)
58 .ok()?
59 .as_secs(),
60 )
61 })
62 .unwrap_or(0);
63
64 let entry = cache
65 .versions
66 .entry(self.cache_key())
67 .or_insert_with(|| PathCacheEntry {
68 timestamp,
69 version: self.version,
70 platform: self.platform,
71 base_path: self.package_dir.clone(),
72 paths: Default::default(),
73 });
74
75 entry.timestamp = timestamp;
76 entry.base_path = self.package_dir.clone();
77 entry.paths.clear();
78
79 for (id, path) in &self.package_paths {
80 entry.paths.insert(*id, path.path.clone());
81 }
82
83 Ok(std::fs::write(
84 exe_relative_path("package_cache.json"),
85 serde_json::to_string_pretty(&cache)?,
86 )?)
87 }
88
89 pub(super) fn validate_cache(
90 version: GameVersion,
91 platform: Option<PackagePlatform>,
92 packages_dir: &Path,
93 ) -> Result<HashMap<u16, String>, String> {
94 if let Some(cache) = Self::read_package_cache(false) {
95 info!("Loading package cache");
96 if let Some(p) = cache
97 .get_paths(version, platform, Some(packages_dir))
98 .ok()
99 .flatten()
100 {
101 let timestamp = fs::metadata(packages_dir)
102 .ok()
103 .and_then(|m| {
104 Some(
105 m.modified()
106 .ok()?
107 .duration_since(SystemTime::UNIX_EPOCH)
108 .ok()?
109 .as_secs(),
110 )
111 })
112 .unwrap_or(0);
113
114 if p.timestamp < timestamp {
115 Err("Package directory changed".to_string())
116 } else if p.base_path != packages_dir {
117 Err("Package directory path changed".to_string())
118 } else {
119 Ok(p.paths.clone())
120 }
121 } else {
122 Err(format!(
123 "No cache entry found for version {version:?}, platform {platform:?}"
124 ))
125 }
126 } else {
127 Err("Failed to load package cache".to_string())
128 }
129 }
130
131 pub fn cache_key(&self) -> String {
134 format!("{}_{}", self.version.id(), self.platform)
135 }
136}
137
138#[derive(serde::Serialize, serde::Deserialize)]
139pub(crate) struct PathCache {
140 cache_version: usize,
141 versions: HashMap<String, PathCacheEntry>,
142}
143
144impl Default for PathCache {
145 fn default() -> Self {
146 Self {
147 cache_version: Self::VERSION,
148 versions: HashMap::default(),
149 }
150 }
151}
152
153impl PathCache {
154 pub const VERSION: usize = 5;
155
156 pub fn get_paths(
160 &self,
161 version: GameVersion,
162 platform: Option<PackagePlatform>,
163 base_path: Option<&Path>,
164 ) -> anyhow::Result<Option<&PathCacheEntry>> {
165 if let Some(platform) = platform {
166 return Ok(self.versions.get(&format!("{}_{}", version.id(), platform)));
167 }
168
169 let mut matches = self
170 .versions
171 .iter()
172 .filter(|(_k, v)| {
173 v.version == version && platform.map(|p| v.platform == p).unwrap_or(true)
174 })
175 .map(|(_, v)| v)
176 .collect_vec();
177
178 if matches.len() > 1 {
179 if let Some(base_path) = base_path {
180 matches.retain(|c| c.base_path == base_path)
181 }
182 }
183
184 if matches.len() > 1 {
185 anyhow::bail!(
186 "There is more than one cache entry for version '{}', but no platform was given",
187 version.name()
188 );
189 }
190
191 Ok(matches.first().copied())
192 }
193}
194
195#[derive(serde::Serialize, serde::Deserialize)]
196pub(crate) struct PathCacheEntry {
197 timestamp: u64,
199 version: GameVersion,
200 platform: PackagePlatform,
201 base_path: PathBuf,
202 paths: HashMap<u16, String>,
203}
204
205pub fn exe_directory() -> PathBuf {
206 std::env::current_exe()
207 .unwrap()
208 .parent()
209 .unwrap()
210 .to_path_buf()
211}
212
213pub fn exe_relative_path(path: &str) -> PathBuf {
214 exe_directory().join(path)
215}