diff --git a/backend/src/procedure/js_scripts.rs b/backend/src/procedure/js_scripts.rs index 991f05157..98738aead 100644 --- a/backend/src/procedure/js_scripts.rs +++ b/backend/src/procedure/js_scripts.rs @@ -333,3 +333,47 @@ async fn js_action_var_arg() { .unwrap() .unwrap(); } + +#[tokio::test] +async fn js_action_test_rename() { + let js_action = JsProcedure { args: vec![] }; + let path: PathBuf = "test/js_action_execute/" + .parse::() + .unwrap() + .canonicalize() + .unwrap(); + let package_id = "test-package".parse().unwrap(); + let package_version: Version = "0.3.0.3".parse().unwrap(); + let name = ProcedureName::Action("test-rename".parse().unwrap()); + let volumes: Volumes = serde_json::from_value(serde_json::json!({ + "main": { + "type": "data" + }, + "compat": { + "type": "assets" + }, + "filebrowser" :{ + "package-id": "filebrowser", + "path": "data", + "readonly": true, + "type": "pointer", + "volume-id": "main", + } + })) + .unwrap(); + let input: Option = None; + let timeout = Some(Duration::from_secs(10)); + js_action + .execute::( + &path, + &package_id, + &package_version, + name, + &volumes, + input, + timeout, + ) + .await + .unwrap() + .unwrap(); +} diff --git a/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js b/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js index 6910afe9d..cb847f796 100644 --- a/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js +++ b/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js @@ -763,6 +763,51 @@ export const action = { async 'js-action-var-arg'(_effects, _input, testInput) { assert(testInput == 42, "Input should be passed in"); + return { + result: { + copyable: false, + message: "Done", + version: "0", + qr: false, + } + } + }, + async 'test-rename'(effects, _input) { + + await effects.writeFile({ + volumeId: 'main', + path: 'test-rename.txt', + toWrite: 'Hello World', + }); + await effects.rename({ + srcVolume: 'main', + srcPath: 'test-rename.txt', + dstVolume: 'main', + dstPath: 'test-rename-2.txt', + }); + + const readIn = await effects.readFile({ + volumeId: 'main', + path: 'test-rename-2.txt', + }); + assert(readIn === 'Hello World', "Contents should be the same"); + + await effects.removeFile({ + path: "test-rename-2.txt", + volumeId: "main", + }); + + try{ + + await effects.removeFile({ + path: "test-rename.txt", + volumeId: "main", + }); + assert(false, "Should not be able to remove file that doesn't exist"); + } + catch (_){} + + return { result: { copyable: false, diff --git a/libs/js_engine/src/artifacts/loadModule.js b/libs/js_engine/src/artifacts/loadModule.js index 3d9a04f9f..3f4432231 100644 --- a/libs/js_engine/src/artifacts/loadModule.js +++ b/libs/js_engine/src/artifacts/loadModule.js @@ -1,6 +1,10 @@ - import Deno from "/deno_global.js"; import * as mainModule from "/embassy.js"; + +function requireParam(param) { + throw new Error(`Missing required parameter ${param}`); +} + /** * This is using the simplified json pointer spec, using no escapes and arrays * @param {object} obj @@ -20,52 +24,83 @@ function jsonPointerValue(obj, pointer) { function maybeDate(value) { if (!value) return value; - return new Date(value) + return new Date(value); } -const writeFile = ({ path, volumeId, toWrite }) => Deno.core.opAsync("write_file", volumeId, path, toWrite); +const writeFile = ( + { + path = requireParam("path"), + volumeId = requireParam("volumeId"), + toWrite = requireParam("toWrite"), + } = requireParam("options"), +) => Deno.core.opAsync("write_file", volumeId, path, toWrite); -const readFile = ({ volumeId, path }) => Deno.core.opAsync("read_file", volumeId, path); -const metadata = async ({ volumeId, path }) => { - const data = await Deno.core.opAsync("metadata", volumeId, path) +const readFile = ( + { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), +) => Deno.core.opAsync("read_file", volumeId, path); +const rename = ( + { + srcVolume = requireParam("srcVolume"), + dstVolume = requireParam("dstVolume"), + srcPath = requireParam("srcPath"), + dstPath = requireParam("dstPath"), + } = requireParam("options"), +) => Deno.core.opAsync("rename", srcVolume, srcPath, dstVolume, dstPath); +const metadata = async ( + { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), +) => { + const data = await Deno.core.opAsync("metadata", volumeId, path); return { ...data, modified: maybeDate(data.modified), created: maybeDate(data.created), accessed: maybeDate(data.accessed), - } + }; }; -const removeFile = ({ volumeId, path }) => Deno.core.opAsync("remove_file", volumeId, path); +const removeFile = ( + { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), +) => Deno.core.opAsync("remove_file", volumeId, path); const isSandboxed = () => Deno.core.opSync("is_sandboxed"); -const writeJsonFile = ({ volumeId, path, toWrite }) => +const writeJsonFile = ( + { + volumeId = requireParam("volumeId"), + path = requireParam("path"), + toWrite = requireParam("toWrite"), + } = requireParam("options"), +) => writeFile({ volumeId, path, toWrite: JSON.stringify(toWrite), }); -const readJsonFile = async ({ volumeId, path }) => JSON.parse(await readFile({ volumeId, path })); -const createDir = ({ volumeId, path }) => Deno.core.opAsync("create_dir", volumeId, path); -const removeDir = ({ volumeId, path }) => Deno.core.opAsync("remove_dir", volumeId, path); -const trace = (x) => Deno.core.opSync("log_trace", x); -const warn = (x) => Deno.core.opSync("log_warn", x); -const error = (x) => Deno.core.opSync("log_error", x); -const debug = (x) => Deno.core.opSync("log_debug", x); -const info = (x) => Deno.core.opSync("log_info", x); -const fetch = async (url, options = null) => { - const {body, ...response} = await Deno.core.opAsync("fetch", url, options); - const textValue = Promise.resolve(body) +const readJsonFile = async ( + { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), +) => JSON.parse(await readFile({ volumeId, path })); +const createDir = ( + { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), +) => Deno.core.opAsync("create_dir", volumeId, path); +const removeDir = ( + { volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"), +) => Deno.core.opAsync("remove_dir", volumeId, path); +const trace = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opSync("log_trace", whatToTrace); +const warn = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opSync("log_warn", whatToTrace); +const error = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opSync("log_error", whatToTrace); +const debug = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opSync("log_debug", whatToTrace); +const info = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opSync("log_info", whatToTrace); +const fetch = async (url = requireParam ('url'), options = null) => { + const { body, ...response } = await Deno.core.opAsync("fetch", url, options); + const textValue = Promise.resolve(body); return { ...response, text() { - return textValue + return textValue; }, json() { - return textValue.then(x => JSON.parse(x)) - } - } + return textValue.then((x) => JSON.parse(x)); + }, + }; }; - const currentFunction = Deno.core.opSync("current_function"); const input = Deno.core.opSync("get_input"); const variable_args = Deno.core.opSync("get_variable_args"); @@ -85,14 +120,15 @@ const effects = { removeFile, createDir, removeDir, - metadata + metadata, + rename, }; const runFunction = jsonPointerValue(mainModule, currentFunction); (async () => { if (typeof runFunction !== "function") { - error(`Expecting ${ currentFunction } to be a function`); - throw new Error(`Expecting ${ currentFunction } to be a function`); + error(`Expecting ${currentFunction} to be a function`); + throw new Error(`Expecting ${currentFunction} to be a function`); } const answer = await runFunction(effects, input, ...variable_args); setState(answer); diff --git a/libs/js_engine/src/lib.rs b/libs/js_engine/src/lib.rs index 695fd763d..4057f5fb9 100644 --- a/libs/js_engine/src/lib.rs +++ b/libs/js_engine/src/lib.rs @@ -259,6 +259,7 @@ impl JsExecutionEnvironment { fns::read_file::decl(), fns::metadata::decl(), fns::write_file::decl(), + fns::rename::decl(), fns::remove_file::decl(), fns::create_dir::decl(), fns::remove_dir::decl(), @@ -529,6 +530,56 @@ mod fns { Ok(()) } #[op] + async fn rename( + state: Rc>, + src_volume: VolumeId, + src_path: PathBuf, + dst_volume: VolumeId, + dst_path: PathBuf, + ) -> Result<(), AnyError> { + let state = state.borrow(); + let ctx: &JsContext = state.borrow(); + let volume_path = ctx + .volumes + .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &src_volume) + .ok_or_else(|| anyhow!("There is no {} in volumes", src_volume))?; + let volume_path_out = ctx + .volumes + .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &dst_volume) + .ok_or_else(|| anyhow!("There is no {} in volumes", dst_volume))?; + if ctx.volumes.readonly(&dst_volume) { + bail!("Volume {} is readonly", dst_volume); + } + + let old_file = volume_path.join(src_path); + let parent_old_file = old_file + .parent() + .ok_or_else(|| anyhow!("Expecting that file is not root"))?; + // With the volume check + if !is_subset(&volume_path, &parent_old_file).await? { + bail!( + "Path '{}' has broken away from parent '{}'", + old_file.to_string_lossy(), + volume_path.to_string_lossy(), + ); + } + + let new_file = volume_path_out.join(dst_path); + let parent_new_file = new_file + .parent() + .ok_or_else(|| anyhow!("Expecting that file is not root"))?; + // With the volume check + if !is_subset(&volume_path_out, &parent_new_file).await? { + bail!( + "Path '{}' has broken away from parent '{}'", + new_file.to_string_lossy(), + volume_path_out.to_string_lossy(), + ); + } + tokio::fs::rename(old_file, new_file).await?; + Ok(()) + } + #[op] async fn remove_file( state: Rc>, volume_id: VolumeId,