Skip to main content

tiger_pkg/manager/
path_cache.rs

1use 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    /// Generates a key unique to the game version + platform combination
134    /// eg. GameVersion::DestinyTheTakenKing and PackagePlatform::PS4 generates cache key "d1_ttk_ps4"
135    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    /// Gets path cache entry by version and platform
159    /// If `platform` is None, the first
160    /// This function will return an error if there are multiple entries for the same version when `platform` is None
161    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 of the packages directory
200    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}