mod_filesystem/
lib.rs

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! {
18/// Caches glob results by glob pattern
19static 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}