1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
use smol::fs;
use std::{env, path::PathBuf};
use ustr::UstrMap;
use crate::{protocol::GameData, util};
/// The cache in which we store data packages to avoid requesting them every
/// time the client starts.
pub struct Cache(PathBuf);
impl Cache {
/// Returns a cache that uses Archipelago's system-wide shared directory.
/// This allows the client to share datapackages with other games, even if
/// they use other client libraries.
pub fn shared() -> Self {
Self(Self::platform_cache_dir().unwrap_or_else(|| {
env::current_dir()
.expect("failed to determine current working directory")
.join("Archipelago")
.join("Cache")
}))
}
/// Returns a cache that uses a custom filesystem path.
pub fn path(path: impl Into<PathBuf>) -> Self {
Self(path.into())
}
/// Returns the default Archipelago cache directory for the current
/// operating system.
fn platform_cache_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.map(|p| p.join("Archipelago").join("Cache"))
}
#[cfg(target_os = "macos")]
{
env::var_os("HOME").map(PathBuf::from).map(|h| {
h.join("Library")
.join("Caches")
.join("Archipelago")
.join("Cache")
})
}
#[cfg(target_os = "linux")]
{
env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| env::home_dir().map(|h| h.join(".cache")))
.map(|p| p.join("Archipelago").join("Cache"))
}
}
/// Returns a map from game names to the cached game data for those games.
///
/// `checksums` is a map from the names of games to load to checksums for
/// each of those games. These checksums indicate the expected versions each
/// game. If the cache doesn't have a [GameData] for a game with a matching
/// checksum, that game won't be returned.
pub(crate) async fn load_data_packages(
&self,
checksums: &UstrMap<String>,
) -> UstrMap<GameData> {
let mut data_packages =
UstrMap::with_capacity_and_hasher(checksums.len(), Default::default());
let dir = self.data_package_path();
for (game, checksum) in checksums {
let path = dir
.join(util::sanitize_file_name(game))
.join(util::sanitize_file_name(format!("{checksum}.json")));
let file = match fs::read_to_string(&path).await {
Ok(f) => f,
Err(err) => {
log::error!("Missing or unreadable cache for {}: {}", game, err);
continue;
}
};
match serde_json::from_str::<GameData>(&file) {
// Double-check that the checksum is accurate
Ok(data) if data.checksum.eq(checksum) => {
data_packages.insert(*game, data);
}
Ok(_) => {}
Err(err) => {
log::error!(
"Failed to deserialize cached data package for {}: {}",
game,
err
);
}
}
}
data_packages
}
/// Stores `data_packages`, a map from game names to data packages, in the
/// cache.
pub(crate) async fn store_data_packages(&self, data_packages: &UstrMap<GameData>) {
let dir = self.data_package_path();
for (game, data) in data_packages {
let game_dir = dir.join(util::sanitize_file_name(game));
if let Err(err) = fs::create_dir_all(&game_dir).await {
log::error!("Failed to create cache directory {game_dir:?}: {err}");
// If one directory fails to create, chances are the others will
// as well.
return;
}
let serialized = match serde_json::to_string(&data) {
Ok(r) => r,
Err(err) => {
log::error!("Failed to serialize data package for {game}: {err}");
continue;
}
};
let path = game_dir.join(util::sanitize_file_name(format!("{}.json", data.checksum)));
if let Err(err) = util::write_file_atomic(&path, serialized).await {
log::error!("Failed to write cached data package to {path:?}: {err}");
}
}
}
/// Returns the subdirectory that should contain datapackages.
fn data_package_path(&self) -> PathBuf {
// We could just use this as the root of the cache, but this is more
// forward-compatible with the possibility of caching other data in the
// future.
self.0.join("datapackage")
}
}
impl Default for Cache {
/// Returns [Cache::shared].
fn default() -> Self {
Cache::shared()
}
}