Skip to main content

thunderstore_engine/profile/
cache.rs

1//! Extract packages into the per-game package cache.
2//!
3//! Mirrors r2modman's `<game>/cache/<full_name>/<version>/` layout. This cache is
4//! the install source and the restore source for re-enabling `State`-tracked
5//! mods, so it must persist across installs.
6
7#![deny(missing_docs)]
8
9use crate::error::{Error, Result};
10use crate::extract::{extract_zip, should_extract};
11use std::path::{Path, PathBuf};
12
13/// The cache directory for one package version:
14/// `<base>/<game>/cache/<full_name>/<version>`.
15pub fn package_cache_dir(base: &Path, game: &str, full_name: &str, version: &str) -> PathBuf {
16  base.join(game).join("cache").join(full_name).join(version)
17}
18
19/// Extracts `archive` into the package cache directory if that version is not
20/// already present, and returns the directory. Uses [`should_extract`] so an
21/// unchanged version is not re-extracted.
22pub fn ensure_extracted(
23  base: &Path,
24  game: &str,
25  full_name: &str,
26  version: &str,
27  archive: &Path,
28) -> Result<PathBuf> {
29  let dir = package_cache_dir(base, game, full_name, version);
30
31  if should_extract(&dir, version)? {
32    extract_zip(archive, &dir)?;
33  }
34
35  if !dir.exists() {
36    return Err(Error::Profile(format!(
37      "package cache {} missing after extraction",
38      dir.display()
39    )));
40  }
41
42  Ok(dir)
43}
44
45#[cfg(test)]
46mod tests {
47  use super::*;
48  use std::io::{Cursor, Write};
49  use tempfile::tempdir;
50  use zip::write::{FileOptions, ZipWriter};
51
52  fn build_zip() -> Vec<u8> {
53    let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
54    let opts: FileOptions<'_, ()> =
55      FileOptions::default().compression_method(zip::CompressionMethod::Stored);
56
57    zip.start_file("manifest.json", opts).unwrap();
58    zip.write_all(br#"{"version_number":"1.0.0"}"#).unwrap();
59    zip.start_file("plugins/Mod.dll", opts).unwrap();
60    zip.write_all(b"dll").unwrap();
61
62    zip.finish().unwrap().into_inner()
63  }
64
65  #[test]
66  fn ensure_extracted_places_and_is_idempotent() {
67    let dir = tempdir().unwrap();
68    let base = dir.path();
69
70    let archive = base.join("Author-Mod-1.0.0.zip");
71    std::fs::write(&archive, build_zip()).unwrap();
72
73    let out = ensure_extracted(base, "valheim", "Author-Mod", "1.0.0", &archive).unwrap();
74
75    assert_eq!(
76      out,
77      package_cache_dir(base, "valheim", "Author-Mod", "1.0.0")
78    );
79    assert!(out.join("plugins/Mod.dll").exists());
80
81    // Second call is a no-op (same version present) and still returns the dir.
82    let again = ensure_extracted(base, "valheim", "Author-Mod", "1.0.0", &archive).unwrap();
83
84    assert!(again.join("plugins/Mod.dll").exists());
85  }
86}