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