1use std::{fs, io};
2
3use assets_manager::{
4 BoxedError,
5 hot_reloading::{EventSender, FsWatcherBuilder},
6 source::{DirEntry, FileContent, FileSystem as RawFs, Source},
7};
8use hashbrown::HashSet;
9
10#[derive(Debug, Clone)]
13pub struct FileSystem {
14 default: RawFs,
15 override_dir: Option<RawFs>,
16}
17
18impl FileSystem {
19 pub fn new() -> io::Result<Self> {
20 let default = RawFs::new(&*super::ASSETS_PATH)?;
21 let override_dir = std::env::var_os("VELOREN_ASSETS_OVERRIDE").and_then(|path| {
22 RawFs::new(path)
23 .map_err(|err| tracing::error!("Error setting override assets directory: {}", err))
24 .ok()
25 });
26
27 let canary = fs::read_to_string(super::ASSETS_PATH.join("common").join("canary.canary"))
28 .map_err(|e| io::Error::other(format!("failed to load canary asset: {}", e)))?;
29
30 if !canary.starts_with("VELOREN_CANARY_MAGIC") {
31 panic!("Canary asset `canary.canary` was present but did not contain the expected data. This *heavily* implies that you've not correctly set up Git LFS (Large File Storage). Visit `https://book.veloren.net/contributors/development-tools.html#git-lfs` for more information about setting up Git LFS.");
32 }
33
34 Ok(Self {
35 default,
36 override_dir,
37 })
38 }
39}
40
41impl Source for FileSystem {
42 fn read(&self, id: &str, ext: &str) -> io::Result<FileContent<'_>> {
43 if let Some(dir) = &self.override_dir {
44 match dir.read(id, ext) {
45 Ok(content) => return Ok(content),
46 Err(err) => {
47 if err.kind() != io::ErrorKind::NotFound {
48 let path = dir.path_of(DirEntry::File(id, ext));
49 tracing::warn!(
50 "Error reading \"{}\": {}. Falling back to default",
51 path.display(),
52 err
53 );
54 }
55 },
56 }
57 }
58
59 self.default.read(id, ext)
61 }
62
63 fn read_dir(&self, id: &str, f: &mut dyn FnMut(DirEntry)) -> io::Result<()> {
64 let mut collected = HashSet::new();
76
77 let mut f = |dir_entry: DirEntry| {
78 let cache_id = match dir_entry {
79 DirEntry::File(path, ext) => (path.to_owned(), Some(ext.to_owned())),
80 DirEntry::Directory(path) => (path.to_owned(), None),
81 };
82
83 if collected.insert(cache_id) {
85 f(dir_entry)
86 }
87 };
88
89 let default_res = self.default.read_dir(id, &mut f);
90 let Some(dir) = &self.override_dir else {
91 return default_res;
93 };
94
95 let override_res = match dir.read_dir(id, &mut f) {
96 Ok(()) => Ok(()),
97 Err(err) => {
98 if err.kind() != io::ErrorKind::NotFound {
99 let path = dir.path_of(DirEntry::Directory(id));
100 tracing::warn!(
101 "Error reading \"{}\": {}. Falling back to default",
102 path.display(),
103 err
104 );
105 }
106 Err(err)
107 },
108 };
109
110 match (default_res, override_res) {
112 (Err(err1), _) if err1.kind() != io::ErrorKind::NotFound => Err(err1),
116 (_, Ok(())) => Ok(()),
118 (Ok(()), Err(_)) => Ok(()),
125 (Err(_), Err(err2)) => Err(err2),
127 }
128 }
129
130 fn exists(&self, entry: DirEntry) -> bool {
131 self.override_dir
132 .as_ref()
133 .is_some_and(|dir| dir.exists(entry))
134 || self.default.exists(entry)
135 }
136
137 fn configure_hot_reloading(&self, events: EventSender) -> Result<(), BoxedError> {
138 let mut builder = FsWatcherBuilder::new()?;
139
140 if let Some(dir) = &self.override_dir {
141 builder.watch(dir.root().to_owned())?;
142 }
143 builder.watch(self.default.root().to_owned())?;
144
145 builder.build(events);
146 Ok(())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::{fs, path::Path};
154
155 pub(super) enum FsNode<'a> {
156 File(&'a str, &'a str),
157 Dir(&'a str, Vec<FsNode<'a>>),
158 }
159
160 impl FileSystem {
161 pub(super) fn scope<R>(f: &dyn Fn(FileSystem, &Path, &Path) -> R) -> R {
162 let tempdir = tempfile::tempdir().expect("failed to get tempdir");
163 let default = RawFs::new(tempdir.path())
164 .expect("failed to create temporary filesystem for assets");
165
166 let tempdir_override =
167 tempfile::tempdir().expect("failed to get tempdir for overrides");
168 let override_dir = RawFs::new(tempdir_override.path())
169 .expect("failed to create temprorary override filesystem");
170
171 let this = Self {
177 default,
178 override_dir: Some(override_dir),
179 };
180
181 f(this, tempdir.path(), tempdir_override.path())
182 }
183
184 pub(super) fn read_to_str(&self, id: &str, ext: &str) -> String {
185 std::str::from_utf8(self.read(id, ext).unwrap().as_ref())
186 .unwrap()
187 .to_owned()
188 }
189
190 pub(super) fn mock_file(dir: &Path, filename: &str, content: &str) {
191 fs::write(dir.join(filename), content).unwrap();
192 }
193
194 pub(super) fn mock_tree(dir: &Path, tree: Vec<FsNode<'_>>) {
195 fn create_mock_node(path: &Path, node: FsNode<'_>) {
196 match node {
197 FsNode::File(name, content) => FileSystem::mock_file(path, name, content),
198 FsNode::Dir(name, entries) => {
199 for entry in entries {
200 fs::create_dir_all(path.join(name)).unwrap();
201 create_mock_node(&path.join(name), entry);
202 }
203 },
204 }
205 }
206
207 for entry in tree {
208 create_mock_node(dir, entry);
209 }
210 }
211 }
212
213 #[test]
216 fn test_mock_tree() {
217 FileSystem::scope(&|fs, main_path, _override_path| {
218 FileSystem::mock_tree(main_path, vec![FsNode::File("template.ron", "(5)")]);
219
220 assert_eq!(fs.read_to_str("template", "ron"), "(5)");
221 })
222 }
223
224 #[test]
225 #[should_panic(expected = "assertion `left == right` failed")]
226 fn test_mock_file_properly_fails() {
227 FileSystem::scope(&|fs, main_path, _override_path| {
228 FileSystem::mock_file(main_path, "template.ron", "(5)");
229
230 assert_eq!(fs.read_to_str("template", "ron"), "(6)");
231 })
232 }
233
234 #[test]
237 fn test_read_main() {
238 FileSystem::scope(&|fs, main_path, _override_path| {
239 FileSystem::mock_file(main_path, "template.ron", "(5)");
240
241 assert_eq!(fs.read_to_str("template", "ron"), "(5)");
242 })
243 }
244
245 #[test]
246 fn test_read_override() {
247 FileSystem::scope(&|fs, _main_path, override_path| {
248 FileSystem::mock_file(override_path, "template.ron", "(5)");
249
250 assert_eq!(fs.read_to_str("template", "ron"), "(5)");
251 })
252 }
253
254 #[test]
255 fn test_read_dir() {
256 FileSystem::scope(&|fs, main_path, _override_path| {
257 #[rustfmt::skip]
258 FileSystem::mock_tree(main_path, vec![
259 FsNode::Dir("entity", vec![
260 FsNode::File("template.ron", "(5)")
261 ]),
262 ]);
263
264 assert_eq!(fs.read_to_str("entity.template", "ron"), "(5)");
265 })
266 }
267
268 #[test]
269 fn test_read_dirfile_override() {
270 FileSystem::scope(&|fs, main_path, override_path| {
271 #[rustfmt::skip]
272 FileSystem::mock_tree(main_path, vec![
273 FsNode::Dir("entity", vec![
274 FsNode::File("template.ron", "(5)")
275 ]),
276 ]);
277
278 #[rustfmt::skip]
279 FileSystem::mock_tree(override_path, vec![
280 FsNode::Dir("entity", vec![
281 FsNode::File("template.ron", "(6)")
282 ]),
283 ]);
284
285 assert_eq!(fs.read_to_str("entity.template", "ron"), "(6)");
286 })
287 }
288
289 #[test]
290 fn test_read_dirfile_override_only() {
291 FileSystem::scope(&|fs, _main_path, override_path| {
292 #[rustfmt::skip]
293 FileSystem::mock_tree(override_path, vec![
294 FsNode::Dir("entity", vec![
295 FsNode::File("template.ron", "(5)")
296 ]),
297 ]);
298
299 assert_eq!(fs.read_to_str("entity.template", "ron"), "(5)");
300 })
301 }
302
303 #[test]
304 fn test_read_dirfile_partial_override() {
305 FileSystem::scope(&|fs, main_path, override_path| {
306 #[rustfmt::skip]
308 FileSystem::mock_tree(main_path, vec![
309 FsNode::Dir("entity", vec![
310 FsNode::File("template.ron", "(5)"),
311 FsNode::File("main.ron", "(7)")
312 ]),
313 ]);
314
315 #[rustfmt::skip]
317 FileSystem::mock_tree(override_path, vec![
318 FsNode::Dir("entity", vec![
319 FsNode::File("template.ron", "(5)")
320 ]),
321 ]);
322
323 assert_eq!(fs.read_to_str("entity.template", "ron"), "(5)");
324 assert_eq!(fs.read_to_str("entity.main", "ron"), "(7)");
325 })
326 }
327
328 #[test]
329 fn test_read_dir_actually() {
339 FileSystem::scope(&|fs, main_path, override_path| {
340 #[rustfmt::skip]
342 FileSystem::mock_tree(main_path, vec![
343 FsNode::Dir("entity", vec![
344 FsNode::File("template.ron", "(5)"),
345 FsNode::File("main.ron", "(7)")
346 ]),
347 ]);
348
349 #[rustfmt::skip]
351 FileSystem::mock_tree(override_path, vec![
352 FsNode::Dir("entity", vec![
353 FsNode::File("template.ron", "(6)"),
354 FsNode::File("fun.ron", "(5)")
355 ]),
356 ]);
357
358 let mut files: Vec<String> = vec![];
359 let _ = fs.read_dir("entity", &mut |e: DirEntry| match e {
360 DirEntry::File(path, ext) => files.push(format!("{path}.{ext}")),
361 DirEntry::Directory(path) => files.push(format!("{path}/")),
362 });
363 files.sort();
364 assert_eq!(files, vec![
365 "entity.fun.ron".to_owned(),
367 "entity.main.ron".to_owned(),
369 "entity.template.ron".to_owned(),
371 ]);
372 })
373 }
374
375 #[test]
376 fn test_read_dir_notfound() {
377 FileSystem::scope(&|fs, main_path, override_path| {
378 #[rustfmt::skip]
380 FileSystem::mock_tree(main_path, vec![
381 FsNode::Dir("entity", vec![FsNode::File(
382 "template.ron",
383 "(5)",
384 )])
385 ]);
386
387 #[rustfmt::skip]
389 FileSystem::mock_tree(override_path, vec![
390 FsNode::Dir("entity", vec![FsNode::File(
391 "template.ron",
392 "(5)",
393 )])
394 ]);
395
396 let res = fs.read("loadout.template", "ron");
401 assert_eq!(res.as_ref().unwrap_err().kind(), io::ErrorKind::NotFound);
402 let msg = format!("{:#?}", res.unwrap_err());
403 if msg.find("loadout/template.ron").is_none() {
404 panic!("error message doesn't contain path:\n{msg}");
405 }
406 })
407 }
408}
409
410#[cfg(test)]
411mod integration {
412 use super::{tests::*, *};
413 use assets_manager::{Asset, AssetCache, FileAsset, SharedString};
414 use hashbrown::HashSet;
415 use serde::Deserialize;
416 use std::borrow::Cow;
417
418 #[derive(Deserialize, Clone, Debug, PartialEq)]
419 struct WowManifest {
420 prefix: usize,
421 }
422
423 #[derive(Deserialize, Clone, Debug, PartialEq)]
424 struct WowFragment {
425 pieces: Vec<usize>,
426 }
427
428 #[derive(Deserialize, Clone, Debug, PartialEq)]
429 struct WowAsset {
430 prefix: usize,
431 pieces: HashSet<usize>,
432 }
433
434 impl FileAsset for WowManifest {
435 const EXTENSION: &'static str = "ron";
436
437 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
438 assets_manager::asset::load_ron(&bytes)
439 }
440 }
441
442 impl FileAsset for WowFragment {
443 const EXTENSION: &'static str = "json";
444
445 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
446 assets_manager::asset::load_json(&bytes)
447 }
448 }
449
450 impl crate::Asset for WowAsset {
452 fn load(cache: &AssetCache, path: &SharedString) -> Result<Self, BoxedError> {
453 let manifest = cache
454 .load::<WowManifest>(&[path, ".", "_manifest"].concat())?
455 .cloned();
456
457 let mut total_pieces = HashSet::new();
458
459 for id in cache.load_rec_dir::<WowFragment>(path)?.read().ids() {
460 match cache.load::<WowFragment>(id) {
461 Ok(handle) => {
462 let WowFragment { pieces } = &handle.read().clone();
463 for piece in pieces {
464 if !total_pieces.insert(*piece) {
465 panic!("duplicate piece ({piece}) in: {id}");
466 }
467 }
468 },
469 Err(err) => panic!("error during loading: {id}\n{err:#?}"),
474 }
475 }
476
477 Ok(Self {
478 prefix: manifest.prefix,
479 pieces: total_pieces,
480 })
481 }
482 }
483
484 #[test]
485 fn test_read_dir() {
486 FileSystem::scope(&|fs, main_path, _override_path| {
487 #[rustfmt::skip]
488 FileSystem::mock_tree(main_path, vec![
489 FsNode::Dir("entity", vec![
490 FsNode::File("_manifest.ron", "(prefix: 5)"),
491 FsNode::File("first.json", r#"{"pieces": [1, 2]}"#),
492 FsNode::File("second.json", r#"{"pieces": [3, 4]}"#),
493 ]),
494 ]);
495
496 let cache = AssetCache::with_source(fs);
497 let asset = WowAsset::load(&cache, &"entity".into()).unwrap();
498 assert_eq!(asset, WowAsset {
499 prefix: 5,
500 pieces: [1, 2, 3, 4].into(),
501 });
502 })
503 }
504
505 #[test]
506 fn test_read_dir_override() {
507 FileSystem::scope(&|fs, main_path, override_path| {
508 #[rustfmt::skip]
509 FileSystem::mock_tree(main_path, vec![
510 FsNode::Dir("entity", vec![
511 FsNode::File("_manifest.ron", "(prefix: 5)"),
512 FsNode::File("first.json", r#"{"pieces": [1, 2]}"#),
513 FsNode::File("second.json", r#"{"pieces": [3, 4]}"#),
514 ]),
515 ]);
516
517 #[rustfmt::skip]
518 FileSystem::mock_tree(override_path, vec![
519 FsNode::Dir("entity", vec![
520 FsNode::File("_manifest.ron", "(prefix: 5)"),
521 FsNode::File("first.json", r#"{"pieces": [5, 6]}"#),
522 FsNode::File("second.json", r#"{"pieces": [3, 4]}"#),
523 ]),
524 ]);
525
526 let cache = AssetCache::with_source(fs);
527 let asset = WowAsset::load(&cache, &"entity".into()).unwrap();
528 assert_eq!(asset, WowAsset {
529 prefix: 5,
530 pieces: [5, 6, 3, 4].into(),
531 });
532 })
533 }
534
535 #[test]
536 fn test_read_dir_partial_override() {
537 FileSystem::scope(&|fs, main_path, override_path| {
538 #[rustfmt::skip]
539 FileSystem::mock_tree(main_path, vec![
540 FsNode::Dir("entity", vec![
541 FsNode::File("_manifest.ron", "(prefix: 5)"),
542 FsNode::File("first.json", r#"{"pieces": [1, 2]}"#),
543 FsNode::File("second.json", r#"{"pieces": [3, 4]}"#),
544 ]),
545 ]);
546
547 #[rustfmt::skip]
548 FileSystem::mock_tree(override_path, vec![
549 FsNode::Dir("entity", vec![
550 FsNode::File("_manifest.ron", "(prefix: 5)"),
551 FsNode::File("first.json", r#"{"pieces": [5, 6]}"#),
553 ]),
554 ]);
555
556 let cache = AssetCache::with_source(fs);
557 let asset = WowAsset::load(&cache, &"entity".into()).unwrap();
558 assert_eq!(asset, WowAsset {
559 prefix: 5,
560 pieces: [5, 6, 3, 4].into(),
561 });
562 })
563 }
564
565 #[test]
566 fn test_read_dir_partial_nested_override() {
567 FileSystem::scope(&|fs, main_path, override_path| {
568 #[rustfmt::skip]
569 FileSystem::mock_tree(main_path, vec![
570 FsNode::Dir("entity", vec![
571 FsNode::File("_manifest.ron", "(prefix: 5)"),
572 FsNode::Dir("nest", vec![
573 FsNode::File("first.json", r#"{"pieces": [1, 2]}"#),
574 FsNode::File("second.json", r#"{"pieces": [3, 4]}"#),
575 ]),
576 ]),
577 ]);
578
579 #[rustfmt::skip]
580 FileSystem::mock_tree(override_path, vec![
581 FsNode::Dir("entity", vec![
582 FsNode::File("_manifest.ron", "(prefix: 7)"),
583 FsNode::Dir("nest", vec![
584 FsNode::File("first.json", r#"{"pieces": [5, 6]}"#),
586 ]),
587 ]),
588 ]);
589
590 let cache = AssetCache::with_source(fs);
591 let asset = WowAsset::load(&cache, &"entity".into()).unwrap();
592 assert_eq!(asset, WowAsset {
593 prefix: 7,
594 pieces: [5, 6, 3, 4].into(),
595 });
596 })
597 }
598}