pacenotes_server/store/
manifest_store.rs1pub struct ManifestStore {
3 data_dir: std::path::PathBuf,
4 cache: std::sync::RwLock<CachedManifest>,
5}
6
7struct CachedManifest {
8 manifest: pacenotes_shared::Manifest,
9 mtime: Option<std::time::SystemTime>,
10 is_loaded: bool,
11}
12
13impl ManifestStore {
14 pub fn new(data_dir: impl Into<std::path::PathBuf>) -> Self {
15 Self {
16 data_dir: data_dir.into(),
17 cache: std::sync::RwLock::new(CachedManifest {
18 manifest: pacenotes_shared::Manifest::default(),
19 mtime: None,
20 is_loaded: false,
21 }),
22 }
23 }
24
25 pub fn manifest_path(&self) -> std::path::PathBuf {
26 self.data_dir.join(crate::store::MANIFEST_FILE)
27 }
28
29 pub fn current(&self) -> pacenotes_shared::Manifest {
32 let disk_mtime = std::fs::metadata(self.manifest_path())
33 .and_then(|metadata| metadata.modified())
34 .ok();
35 {
36 let cached = self.cache.read().unwrap_or_else(|e| e.into_inner());
37 if cached.is_loaded && cached.mtime == disk_mtime {
38 return cached.manifest.clone();
39 }
40 }
41
42 match crate::store::manifest_file::read_manifest(&self.manifest_path()) {
43 Ok(manifest) => {
44 let mut cached = self.cache.write().unwrap_or_else(|e| e.into_inner());
45 cached.manifest = manifest.clone();
46 cached.mtime = disk_mtime;
47 cached.is_loaded = true;
48 manifest
49 }
50 Err(e) => {
51 tracing::error!(
52 error = %e,
53 path = %self.manifest_path().display(),
54 "manifest load failed"
55 );
56 self.cache
57 .read()
58 .unwrap_or_else(|e| e.into_inner())
59 .manifest
60 .clone()
61 }
62 }
63 }
64
65 pub fn zip_path(
68 &self,
69 game: &str,
70 directory: &str,
71 name: &str,
72 ) -> Option<(pacenotes_shared::PackageInfo, std::path::PathBuf)> {
73 let package = self.current().packages.into_iter().find(|package| {
74 package.game == game && package.directory == directory && package.name == name
75 })?;
76 let path = zip_file(&self.data_dir, &package)?;
77 Some((package, path))
78 }
79
80 pub fn motd(&self, lang: &str) -> Option<String> {
81 crate::store::motd::read_motd(&self.data_dir, lang)
82 }
83}
84
85pub fn zip_file(
88 data_dir: &std::path::Path,
89 package: &pacenotes_shared::PackageInfo,
90) -> Option<std::path::PathBuf> {
91 if !pacenotes_shared::valid_id(&package.game)
92 || !pacenotes_shared::valid_id(&package.directory)
93 || !pacenotes_shared::valid_name(&package.name)
94 {
95 return None;
96 }
97 let path = data_dir
98 .join(&package.game)
99 .join(crate::store::PACKAGES_DIR)
100 .join(&package.directory)
101 .join(format!("{}.zip", package.name));
102 Some(path)
103}
104
105#[cfg(test)]
106mod tests {
107 #[test]
108 fn reloads_on_mtime_change_and_resolves_paths() {
109 let tmp = tempfile::tempdir().unwrap();
110 let dir = tmp.path();
111 crate::store::fixture_tree::write_package(dir, "rbr", "public/basic.zip", b"hello");
112 let store = super::ManifestStore::new(dir);
113 assert!(store.current().packages.is_empty());
114
115 crate::store::write_manifest(dir, &crate::store::fixture_tree::generate(dir).manifest)
116 .unwrap();
117
118 assert_eq!(store.current().packages.len(), 1);
119 let (package, path) = store.zip_path("rbr", "public", "basic").unwrap();
120 assert_eq!(package.name, "basic");
121 assert_eq!(
122 path,
123 crate::store::fixture_tree::package_path(dir, "rbr", "public/basic.zip")
124 );
125 assert!(store.zip_path("rbr", "public", "nope").is_none());
126 assert!(store.zip_path("rbr", "../data", "auth").is_none());
127 }
128
129 #[test]
130 fn nested_names_resolve_back_to_their_zip() {
131 let tmp = tempfile::tempdir().unwrap();
132 let dir = tmp.path();
133 let kamil = crate::store::fixture_tree::package_path(
134 dir,
135 "rbr",
136 "public/codriver/polish/male/Kamil.zip",
137 );
138 crate::store::fixture_tree::write_package(
139 dir,
140 "rbr",
141 "public/codriver/polish/male/Kamil.zip",
142 b"kamil",
143 );
144 crate::store::write_manifest(dir, &crate::store::fixture_tree::generate(dir).manifest)
145 .unwrap();
146
147 let store = super::ManifestStore::new(dir);
148 let (package, path) = store
149 .zip_path("rbr", "public", "codriver/polish/male/Kamil")
150 .unwrap();
151 assert_eq!(package.leaf(), "Kamil");
152 assert_eq!(path, kamil);
153
154 for not_a_package in [
155 "codriver/polish/male",
156 "codriver",
157 "codriver/../../manifest",
158 ] {
159 assert!(
160 store.zip_path("rbr", "public", not_a_package).is_none(),
161 "{not_a_package}"
162 );
163 }
164 }
165
166 #[test]
167 fn a_bad_manifest_keeps_the_last_good_copy_serving() {
168 let tmp = tempfile::tempdir().unwrap();
169 let dir = tmp.path();
170 crate::store::fixture_tree::write_package(dir, "rbr", "public/basic.zip", b"hello");
171 crate::store::write_manifest(dir, &crate::store::fixture_tree::generate(dir).manifest)
172 .unwrap();
173 let store = super::ManifestStore::new(dir);
174 assert_eq!(store.current().packages.len(), 1);
175
176 crate::store::fixture_tree::write(&dir.join(crate::store::MANIFEST_FILE), b"{ not json");
177 assert_eq!(store.current().packages.len(), 1);
178 }
179}