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