1use crate::error::{CacheError, FilesystemError};
2use crate::fs::{config_dir, PersistentData};
3use crate::parse::Fingerprint;
4use crate::repository::RepositoryKind;
5use crate::{
6 addon::{Addon, AddonFolder},
7 catalog::{download_catalog, Catalog},
8};
9use crate::{config::Flavor, error::DownloadError};
10
11use async_std::fs::rename;
12use async_std::sync::{Arc, Mutex};
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15
16use std::collections::HashMap;
17use std::convert::TryFrom;
18use std::path::PathBuf;
19
20#[derive(Serialize, Deserialize, Default, Debug)]
21pub struct FingerprintCache(HashMap<Flavor, Vec<Fingerprint>>);
22
23impl FingerprintCache {
24 pub(crate) fn get_mut_for_flavor(&mut self, flavor: Flavor) -> &mut Vec<Fingerprint> {
25 self.0.entry(flavor).or_default()
26 }
27
28 pub(crate) fn flavor_exists(&self, flavor: Flavor) -> bool {
29 self.0.contains_key(&flavor)
30 }
31
32 pub(crate) fn delete_flavor(&mut self, flavor: Flavor) {
33 self.0.remove_entry(&flavor);
34 }
35}
36
37impl PersistentData for FingerprintCache {
38 fn relative_path() -> PathBuf {
39 PathBuf::from("cache/fingerprints.yml")
40 }
41}
42
43pub async fn load_fingerprint_cache() -> Result<FingerprintCache, CacheError> {
44 {
46 let old_location = config_dir().join("fingerprints.yml");
47
48 if old_location.exists() {
49 let new_location = FingerprintCache::path()?;
50
51 rename(old_location, new_location)
52 .await
53 .map_err(FilesystemError::Io)?;
54 }
55 }
56
57 Ok(FingerprintCache::load_or_default()?)
58}
59
60#[derive(Serialize, Deserialize, Debug)]
61pub enum AddonCache {
62 V1(HashMap<Flavor, Vec<AddonCacheEntry>>),
63}
64
65impl Default for AddonCache {
66 fn default() -> Self {
67 AddonCache::V1(Default::default())
68 }
69}
70
71impl AddonCache {
72 pub(crate) fn get_mut_for_flavor(&mut self, flavor: Flavor) -> &mut Vec<AddonCacheEntry> {
73 match self {
74 AddonCache::V1(cache) => cache.entry(flavor).or_default(),
75 }
76 }
77}
78
79impl PersistentData for AddonCache {
80 fn relative_path() -> PathBuf {
81 PathBuf::from("cache/addons.yml")
82 }
83}
84
85pub async fn load_addon_cache() -> Result<AddonCache, CacheError> {
86 Ok(AddonCache::load_or_default()?)
87}
88
89pub async fn update_addon_cache(
93 addon_cache: Arc<Mutex<AddonCache>>,
94 entry: AddonCacheEntry,
95 flavor: Flavor,
96) -> Result<AddonCacheEntry, CacheError> {
97 let mut addon_cache = addon_cache.lock().await;
99
100 let entries = addon_cache.get_mut_for_flavor(flavor);
102
103 entries.retain(|e| !(e.folder_names == entry.folder_names || e.title == entry.title));
105
106 entries.push(entry.clone());
108
109 addon_cache.save()?;
111
112 Ok(entry)
113}
114
115pub async fn remove_addon_cache_entry(
118 addon_cache: Arc<Mutex<AddonCache>>,
119 entry: AddonCacheEntry,
120 flavor: Flavor,
121) -> Result<Option<AddonCacheEntry>, CacheError> {
122 let mut addon_cache = addon_cache.lock().await;
124
125 let entries = addon_cache.get_mut_for_flavor(flavor);
127
128 if let Some(idx) = entries
130 .iter()
131 .position(|e| e.folder_names == entry.folder_names || e.title == entry.title)
132 {
133 let entry = entries.remove(idx);
134
135 addon_cache.save()?;
137
138 Ok(Some(entry))
139 } else {
140 Ok(None)
141 }
142}
143
144pub async fn remove_addon_entries_with_missing_folders(
149 addon_cache: Arc<Mutex<AddonCache>>,
150 flavor: Flavor,
151 folders: &[AddonFolder],
152 save_cache: bool,
153) -> Result<usize, CacheError> {
154 let folder_names = folders.iter().map(|f| f.id.clone()).collect::<Vec<_>>();
156
157 let mut addon_cache = addon_cache.lock().await;
159
160 let entries = addon_cache.get_mut_for_flavor(flavor);
162
163 let entries_to_delete = entries
166 .iter()
167 .cloned()
168 .enumerate()
169 .filter(|(_, entry)| !entry.folder_names.iter().all(|f| folder_names.contains(&f)))
170 .map(|(idx, _)| idx)
171 .collect::<Vec<_>>();
172
173 if !entries_to_delete.is_empty() {
174 for (offset, idx) in entries_to_delete.iter().enumerate() {
177 entries.remove(*idx - offset);
178 }
179
180 if save_cache {
182 addon_cache.save()?;
183 }
184 }
185
186 Ok(entries_to_delete.len())
188}
189
190#[derive(Serialize, Deserialize, Clone, Debug)]
191pub struct AddonCacheEntry {
192 pub title: String,
193 pub repository: RepositoryKind,
194 pub repository_id: String,
195 pub primary_folder_id: String,
196 pub folder_names: Vec<String>,
197 pub modified: DateTime<Utc>,
198 pub external_release_id: Option<ExternalReleaseId>,
199}
200
201impl TryFrom<&Addon> for AddonCacheEntry {
202 type Error = CacheError;
203
204 fn try_from(addon: &Addon) -> Result<Self, CacheError> {
205 if let (Some(repository), Some(repository_id)) =
206 (addon.repository_kind(), addon.repository_id())
207 {
208 let mut folder_names: Vec<_> = addon.folders.iter().map(|a| a.id.clone()).collect();
209 folder_names.sort();
210
211 let external_release_id = if repository == RepositoryKind::TownlongYak {
212 addon.file_id().map(ExternalReleaseId::FileId)
213 } else {
214 addon
215 .version()
216 .map(str::to_string)
217 .map(ExternalReleaseId::Version)
218 };
219
220 Ok(AddonCacheEntry {
221 title: addon.title().to_owned(),
222 repository,
223 repository_id: repository_id.to_owned(),
224 primary_folder_id: addon.primary_folder_id.clone(),
225 folder_names,
226 modified: Utc::now(),
227 external_release_id,
228 })
229 } else {
230 Err(CacheError::AddonMissingRepo {
231 title: addon.title().to_owned(),
232 })
233 }
234 }
235}
236
237#[derive(Serialize, Deserialize, Clone, Debug)]
238pub enum ExternalReleaseId {
239 FileId(i64),
240 Version(String),
241}
242
243#[derive(Serialize, Deserialize, Debug)]
244pub struct CatalogCache {
245 etag: String,
246 catalog: Catalog,
247}
248
249impl PersistentData for CatalogCache {
250 fn relative_path() -> PathBuf {
251 PathBuf::from("cache/catalog.yml")
252 }
253}
254
255pub async fn catalog_download_latest_or_use_cache() -> Result<Catalog, DownloadError> {
256 let maybe_cached_catalog = CatalogCache::load();
257
258 let cached_etag = maybe_cached_catalog.as_ref().map(|c| c.etag.clone()).ok();
261
262 if let Some((downloaded_etag, downloaded_catalog)) = download_catalog(cached_etag).await? {
263 if let Some(etag) = downloaded_etag {
266 let new_cache = CatalogCache {
268 catalog: downloaded_catalog.clone(),
269 etag,
270 };
271 new_cache.save()?;
272 }
273
274 Ok(downloaded_catalog)
275 } else {
276 let cache = maybe_cached_catalog?;
278
279 Ok(cache.catalog)
280 }
281}
282
283#[cfg(test)]
284mod test {
285 use super::*;
286 use crate::repository::RepositoryIdentifiers;
287
288 use async_std::task;
289
290 #[test]
291 fn test_remove_entries_with_missing_folders() {
292 task::block_on(async {
293 let flavor = Flavor::Retail;
294
295 let addon_folders = (0..30)
296 .map(|idx| AddonFolder {
297 id: format!("folder_{}", idx + 1),
298 title: format!("folder_{}", idx + 1),
299 interface: Default::default(),
300 path: Default::default(),
301 author: Default::default(),
302 notes: Default::default(),
303 version: Default::default(),
304 repository_identifiers: RepositoryIdentifiers {
305 curse: Some(idx as i32),
306 ..Default::default()
307 },
308 dependencies: Default::default(),
309 fingerprint: Default::default(),
310 })
311 .collect::<Vec<_>>();
312
313 let cache = {
314 let cache: Arc<Mutex<AddonCache>> = Default::default();
315 let mut cache_lock = cache.lock_arc().await;
316
317 let entries = cache_lock.get_mut_for_flavor(flavor);
318
319 entries.extend(addon_folders.chunks(10).enumerate().map(|(idx, folders)| {
320 AddonCacheEntry {
321 title: format!("Test{}", idx + 1),
322 repository: RepositoryKind::Tukui,
323 repository_id: format!("{}", idx + 1),
324 primary_folder_id: folders.first().map(|f| f.id.clone()).unwrap(),
325 folder_names: folders.iter().map(|f| f.id.clone()).collect(),
326 modified: Utc::now(),
327 external_release_id: None,
328 }
329 }));
330
331 cache
332 };
333
334 let num_deleted = remove_addon_entries_with_missing_folders(
338 cache.clone(),
339 flavor,
340 &addon_folders[5..20],
341 false,
342 )
343 .await
344 .unwrap();
345
346 assert_eq!(num_deleted, 2);
347
348 let mut cache_lock = cache.lock().await;
349
350 let entries = cache_lock.get_mut_for_flavor(flavor);
351
352 let names = entries.iter().map(|e| e.title.clone()).collect::<Vec<_>>();
353
354 assert_eq!(names, vec!["Test2".to_string()]);
355 });
356 }
357}