1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
use anyhow::anyhow;
use config::get_or_create_module;
use lruttl::LruCacheWithTtl;
use mlua::Lua;
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant};

const GLOB_CACHE_CAPACITY: usize = 32;
const DEFAULT_CACHE_TTL: f32 = 60.;

#[derive(PartialEq, Eq, Hash)]
struct GlobKey {
    pattern: String,
    path: Option<String>,
}

static CACHE: LazyLock<Arc<LruCacheWithTtl<GlobKey, Result<Vec<String>, String>>>> =
    LazyLock::new(|| make_cache());

fn make_cache() -> Arc<LruCacheWithTtl<GlobKey, Result<Vec<String>, String>>> {
    Arc::new(LruCacheWithTtl::new_named(
        "mod_filesystem_glob_cache",
        GLOB_CACHE_CAPACITY,
    ))
}

pub fn register(lua: &Lua) -> anyhow::Result<()> {
    let kumo_mod = get_or_create_module(lua, "kumo")?;
    kumo_mod.set("read_dir", lua.create_async_function(read_dir)?)?;
    kumo_mod.set("glob", lua.create_async_function(cached_glob)?)?;
    kumo_mod.set("uncached_glob", lua.create_async_function(uncached_glob)?)?;
    Ok(())
}

async fn read_dir<'lua>(_: &'lua Lua, path: String) -> mlua::Result<Vec<String>> {
    let mut dir = tokio::fs::read_dir(path)
        .await
        .map_err(mlua::Error::external)?;
    let mut entries = vec![];
    while let Some(entry) = dir.next_entry().await.map_err(mlua::Error::external)? {
        if let Some(utf8) = entry.path().to_str() {
            entries.push(utf8.to_string());
        } else {
            return Err(mlua::Error::external(anyhow!(
                "path entry {} is not representable as utf8",
                entry.path().display()
            )));
        }
    }
    Ok(entries)
}

async fn cached_glob<'lua>(
    _: &'lua Lua,
    (pattern, path, ttl): (String, Option<String>, Option<f32>),
) -> mlua::Result<Vec<String>> {
    let key = GlobKey {
        pattern: pattern.to_string(),
        path: path.clone(),
    };
    if let Some(cached) = CACHE.get(&key) {
        return cached.map_err(mlua::Error::external);
    }

    let result = glob(pattern.clone(), path.clone())
        .await
        .map_err(|err| format!("glob({pattern}, {path:?}): {err:#}"));

    let ttl = Duration::from_secs_f32(ttl.unwrap_or(DEFAULT_CACHE_TTL));

    CACHE
        .insert(key, result.clone(), Instant::now() + ttl)
        .map_err(mlua::Error::external)
}

async fn uncached_glob<'lua>(
    _: &'lua Lua,
    (pattern, path): (String, Option<String>),
) -> mlua::Result<Vec<String>> {
    glob(pattern, path).await
}

async fn glob(pattern: String, path: Option<String>) -> mlua::Result<Vec<String>> {
    let entries = tokio::task::spawn_blocking(move || {
        let mut entries = vec![];
        let glob = filenamegen::Glob::new(&pattern)?;
        for path in glob.walk(path.as_deref().unwrap_or(".")) {
            if let Some(utf8) = path.to_str() {
                entries.push(utf8.to_string());
            } else {
                return Err(anyhow!(
                    "path entry {} is not representable as utf8",
                    path.display()
                ));
            }
        }
        Ok(entries)
    })
    .await
    .map_err(mlua::Error::external)?
    .map_err(mlua::Error::external)?;
    Ok(entries)
}