Actor-served files: /actor/<id>/fs/
ci / check (push) Successful in 10s

File operations under /actor/<id>/fs/ become messages from `fs` to <id>
(fs_read, fs_write, fs_mkdir, fs_remove, fs_rename) with a 10 s timeout.
vfs.rs wraps the wasi:filesystem implementation of wasmtime-wasi.
This commit is contained in:
Sausage Agent
2026-10-16 01:25:11 +00:00
parent e1409b93f2
commit 2ff30ecf70
8 changed files with 866 additions and 4 deletions
+36
View File
@@ -39,6 +39,8 @@ fn main() {
}
let mut open = HashMap::<String, std::fs::File>::new();
// Files that this actor serves at /actor/<id>/fs/ (path -> text).
let mut served = std::collections::BTreeMap::<String, String>::new();
let stdin = io::stdin();
let mut out = io::stdout().lock();
let mut emit_shutdown = false;
@@ -73,6 +75,40 @@ fn main() {
break;
}
Some("echo") => json!({"type": "echo", "value": body["value"]}),
Some("fs_read") => {
let dir = if path == "/" { "/".to_string() } else { format!("{path}/") };
let mut names: Vec<String> = served
.keys()
.filter_map(|k| k.strip_prefix(&dir))
.map(|rest| match rest.split_once('/') {
Some((first, _)) => format!("{first}/"),
None => rest.to_string(),
})
.collect();
names.dedup();
if let Some(text) = served.get(&path) {
json!({"type": "fs_file", "text": text})
} else if path == "/" || !names.is_empty() {
let entries: Vec<Value> = names
.iter()
.map(|n| match n.strip_suffix('/') {
Some(d) => json!({"name": d, "type": "dir"}),
None => json!({"name": n, "type": "file"}),
})
.collect();
json!({"type": "fs_dir", "entries": entries})
} else {
json!({"type": "fs_error", "errno": "no-entry"})
}
}
Some("fs_write") => {
served.insert(path.clone(), text.to_string());
json!({"type": "fs_done"})
}
Some("fs_remove") => match served.remove(&path) {
Some(_) => json!({"type": "fs_done"}),
None => json!({"type": "fs_error", "errno": "no-entry"}),
},
Some("publish") => {
send(
&mut out,
+30
View File
@@ -216,6 +216,36 @@ async fn actors_can_read_but_not_write_other_actor_data() {
host.stop().await;
}
#[tokio::test]
async fn actors_serve_files_under_fs() {
// "writer" serves its fs/ from memory (toolbox fs_read / fs_write / fs_remove).
let (_, host, mut mailbox) = filesystem_world("actor-fs").await;
let file = "/actor/writer/fs/notes/a.txt";
let write = json!({"type": "write", "path": file, "text": "hello"});
ask_type(&host, &mut mailbox, "reader", write, "ok").await;
// The write reached the owner as one fs_write message.
let direct = json!({"type": "fs_read", "path": "/notes/a.txt"});
let served = ask_type(&host, &mut mailbox, "writer", direct, "fs_file").await;
assert_eq!(served.body["text"], "hello");
let read = json!({"type": "read", "path": file});
assert_eq!(ask_type(&host, &mut mailbox, "reader", read, "content").await.body["text"], "hello");
let list = json!({"type": "list", "path": "/actor/writer/fs/notes"});
assert_eq!(ask_type(&host, &mut mailbox, "reader", list, "entries").await.body["names"], json!(["a.txt"]));
let root = json!({"type": "list", "path": "/actor/writer/fs"});
assert_eq!(ask_type(&host, &mut mailbox, "reader", root, "entries").await.body["names"], json!(["notes"]));
// fs_error and undeliverable become errors of the file operation.
for path in ["/actor/writer/fs/missing.txt", "/actor/nobody/fs/x"] {
ask_type(&host, &mut mailbox, "reader", json!({"type": "read", "path": path}), "error").await;
}
// An actor that does not answer (here: it is busy reading itself) fails after the timeout.
let id = host.broker.send("console", Some("reader"), None, None, json!({"type": "read", "path": "/actor/reader/fs/x"}));
let timed_out = receive_reply(&mut mailbox, &id, Duration::from_secs(20)).await;
assert_eq!(timed_out.body["type"], "error");
host.stop().await;
}
#[tokio::test]
async fn files_are_plain_posix_files() {
let (_, host, mut mailbox) = filesystem_world("posix").await;