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