1use anyhow::anyhow;
2use config::get_or_create_module;
3use mlua::Lua;
4use tokio::time::{Duration, Instant};
5
6const GLOB_CACHE_CAPACITY: usize = 32;
7const DEFAULT_CACHE_TTL: f32 = 60.;
8
9#[derive(PartialEq, Eq, Hash, Clone, Debug)]
10struct GlobKey {
11 pattern: String,
12 path: Option<String>,
13}
14
15lruttl::declare_cache! {
16static CACHE: LruCacheWithTtl<GlobKey, Result<Vec<String>, String>>::new("mod_filesystem_glob_cache", GLOB_CACHE_CAPACITY);
17}
18
19pub fn register(lua: &Lua) -> anyhow::Result<()> {
20 let kumo_mod = get_or_create_module(lua, "kumo")?;
21 kumo_mod.set("read_dir", lua.create_async_function(read_dir)?)?;
22 kumo_mod.set("glob", lua.create_async_function(cached_glob)?)?;
23 kumo_mod.set("uncached_glob", lua.create_async_function(uncached_glob)?)?;
24 Ok(())
25}
26
27async fn read_dir(_: Lua, path: String) -> mlua::Result<Vec<String>> {
28 let mut dir = tokio::fs::read_dir(path)
29 .await
30 .map_err(mlua::Error::external)?;
31 let mut entries = vec![];
32 while let Some(entry) = dir.next_entry().await.map_err(mlua::Error::external)? {
33 if let Some(utf8) = entry.path().to_str() {
34 entries.push(utf8.to_string());
35 } else {
36 return Err(mlua::Error::external(anyhow!(
37 "path entry {} is not representable as utf8",
38 entry.path().display()
39 )));
40 }
41 }
42 Ok(entries)
43}
44
45async fn cached_glob(
46 _: Lua,
47 (pattern, path, ttl): (String, Option<String>, Option<f32>),
48) -> mlua::Result<Vec<String>> {
49 let key = GlobKey {
50 pattern: pattern.to_string(),
51 path: path.clone(),
52 };
53 if let Some(cached) = CACHE.get(&key) {
54 return cached.map_err(mlua::Error::external);
55 }
56
57 let result = glob(pattern.clone(), path.clone())
58 .await
59 .map_err(|err| format!("glob({pattern}, {path:?}): {err:#}"));
60
61 let ttl = Duration::from_secs_f32(ttl.unwrap_or(DEFAULT_CACHE_TTL));
62
63 CACHE
64 .insert(key, result.clone(), Instant::now() + ttl)
65 .await
66 .map_err(mlua::Error::external)
67}
68
69async fn uncached_glob(
70 _: Lua,
71 (pattern, path): (String, Option<String>),
72) -> mlua::Result<Vec<String>> {
73 glob(pattern, path).await
74}
75
76async fn glob(pattern: String, path: Option<String>) -> mlua::Result<Vec<String>> {
77 let entries = tokio::task::spawn_blocking(move || {
78 let mut entries = vec![];
79 let glob = filenamegen::Glob::new(&pattern)?;
80 for path in glob.walk(path.as_deref().unwrap_or(".")) {
81 if let Some(utf8) = path.to_str() {
82 entries.push(utf8.to_string());
83 } else {
84 return Err(anyhow!(
85 "path entry {} is not representable as utf8",
86 path.display()
87 ));
88 }
89 }
90 Ok(entries)
91 })
92 .await
93 .map_err(mlua::Error::external)?
94 .map_err(mlua::Error::external)?;
95 Ok(entries)
96}