mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
fix(web): scripts can open files in browser storage; stored footage isn't reported missing
In the browser editor a .jsx script couldn't open any file, even one the
page holds next to the project: File.open("r") returned false with "File
not found" and File.exists was false. The footage check after File > Open
also marked imported media as missing.
Both asked whether a file exists on the local disk (std::fs), which has no
files on wasm32:
- scripts read files through Session.services but checked existence with
std::fs (File.open's access check, File.exists, Folder.exists); they now
ask services.exists / services.is_dir, after the folder check;
- WebServices kept the default exists (std::fs); it now looks in the file
table.
Desktop and CLI are unchanged (FsServices keeps the std::fs defaults).
Tests: script tests_services (a host with in-memory files), web smoke
step hostFiles (a script reads a stored file, footage check finds stored
media).
This commit is contained in:
@@ -144,6 +144,10 @@ impl Services for WebServices {
|
||||
let dir = dir.trim_end_matches('/');
|
||||
list().into_iter().map(|(p, _)| p).filter(|p| p.rsplit_once('/').is_some_and(|(d, _)| d == dir)).collect()
|
||||
}
|
||||
/// The table's files (the default asks `std::fs`, which has no files on wasm32).
|
||||
fn exists(&self, path: &str) -> bool {
|
||||
get(path).is_some()
|
||||
}
|
||||
fn is_dir(&self, _path: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -441,6 +441,18 @@ try {
|
||||
const comps = await js(`effectcraft.inspect().then(i => i.activeComp)`);
|
||||
if (!comps) throw new Error("reopened project has no active comp");
|
||||
report.steps.reopen = { ...opened, activeComp: comps };
|
||||
|
||||
// Files the page holds (browser storage, no std::fs): a script opens one next to the project,
|
||||
// and the footage check finds the imported media instead of marking it missing.
|
||||
const hf = await js(`(async () => {
|
||||
const p = await effectcraft.addFile(new File(["from the page"], "script-data.txt"));
|
||||
await effectcraft.saveToBrowser("/host-files.ecproj");
|
||||
const run = await effectcraft.execute("script.run", {code: "var f = new File(" + JSON.stringify(p) + "); writeLn(f.open('r') ? f.read() : f.error);"});
|
||||
return {output: run.output, footage: await effectcraft.execute("footage.check", {wait: true})};
|
||||
})()`);
|
||||
report.steps.hostFiles = hf;
|
||||
check(hf.output === "from the page", `a script opens a file the page holds (${hf.output})`);
|
||||
check(hf.footage.checked > 0 && hf.footage.missing === 0, `stored footage isn't missing (${JSON.stringify(hf.footage)})`);
|
||||
report.steps.final = { screenshot: await shot("02-after") };
|
||||
|
||||
// Persistence: the session, imported media, settings and a project saved to browser storage
|
||||
|
||||
@@ -107,4 +107,6 @@ mod tests_ui_more;
|
||||
#[cfg(test)]
|
||||
mod tests_evalfile;
|
||||
#[cfg(test)]
|
||||
mod tests_services;
|
||||
#[cfg(test)]
|
||||
mod tests_ui_mouse;
|
||||
|
||||
@@ -380,11 +380,14 @@ fn native_file(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsV
|
||||
}
|
||||
let s = &a.session;
|
||||
match op.as_str() {
|
||||
"access" => Ok(json!(check_access(s, &path, false)?.exists())),
|
||||
// Through the session's services, like the reads: hosts without a file system (the
|
||||
// browser's file table) answer for their own files.
|
||||
"access" => Ok(json!(s.services.exists(&check_access(s, &path, false)?.to_string_lossy()))),
|
||||
"exists" | "isDir" => {
|
||||
// Unreadable locations report "missing" instead of failing.
|
||||
let Ok(p) = check_access(s, &path, false) else { return Ok(json!(false)) };
|
||||
Ok(json!(if op == "exists" { p.exists() } else { p.is_dir() }))
|
||||
let p = p.to_string_lossy();
|
||||
Ok(json!(if op == "exists" { s.services.exists(&p) } else { s.services.is_dir(&p) }))
|
||||
}
|
||||
"read" | "readBinary" => {
|
||||
let p = check_access(s, &path, false)?;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Scripts see the host's files: existence checks go through `Services`, like reads, so a host
|
||||
//! that keeps files in memory (the web editor's browser storage) answers for its own files.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use effectcraft_engine::{Services, Session};
|
||||
|
||||
use crate::run_code;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Mem(Mutex<HashMap<String, Vec<u8>>>);
|
||||
|
||||
impl Services for Mem {
|
||||
fn read_file(&self, path: &str) -> std::io::Result<Vec<u8>> {
|
||||
self.0.lock().unwrap().get(path).cloned().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, path.to_string()))
|
||||
}
|
||||
fn write_file(&self, path: &str, data: &[u8]) -> std::io::Result<()> {
|
||||
self.0.lock().unwrap().insert(path.into(), data.into());
|
||||
Ok(())
|
||||
}
|
||||
fn exists(&self, path: &str) -> bool {
|
||||
self.0.lock().unwrap().contains_key(path)
|
||||
}
|
||||
fn is_dir(&self, path: &str) -> bool {
|
||||
path == "/browser/footage"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_script_opens_a_file_only_the_host_holds() {
|
||||
let mem = Arc::new(Mem::default());
|
||||
mem.write_file("/browser/data.txt", b"hello").unwrap();
|
||||
let mut s = Session { services: mem, ..Default::default() };
|
||||
crate::install(&mut s);
|
||||
// A project in a folder that isn't on disk: scripts may read files next to it.
|
||||
s.path = Some("/browser/project.ecproj".into());
|
||||
let out = run_code(
|
||||
&mut s,
|
||||
r#"var f = new File("/browser/data.txt"); var ok = f.open("r");
|
||||
writeLn(ok + " " + (ok ? f.read() : f.error));
|
||||
writeLn(new File("/browser/missing.txt").exists + " " + new Folder("/browser/footage").exists);"#,
|
||||
"t.jsx",
|
||||
);
|
||||
assert!(out.error.is_none(), "{:?}", out.error);
|
||||
assert_eq!(out.output, vec!["true hello".to_string(), "false true".to_string()]);
|
||||
}
|
||||
+2
-1
@@ -413,7 +413,8 @@ no npm packages): load, the GPU path (WebGPU → GPU compositor, viewer on the G
|
||||
in the viewer, `render.frame`, Render Queue GIF and PNG sequence (`.zip`) downloads, a background
|
||||
render in a worker while measuring the page's event-loop gaps (must stay under 400 ms) and its
|
||||
progress, the AudioContext starting on a (simulated) key press and a tone playing through it
|
||||
with the meters moving, Save As and re-opening the saved project, persistence across a reload
|
||||
with the meters moving, Save As and re-opening the saved project, a script opening a file from
|
||||
browser storage and the footage check finding stored media, persistence across a reload
|
||||
(session snapshot, a stored project, imported media, a setting, Open Recent) and an offline
|
||||
reload through the service worker. It writes screenshots and `report.json`:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user