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