mod_filesystem/
lib.rs

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}