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! {
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}