浏览代码

feat: fs.readdir, and a workflow id a node can see

fszontagh 1 月之前
父节点
当前提交
18d98f390d
共有 3 个文件被更改,包括 109 次插入 和 1 次删除
  1. 85 1
      src/runner/engine/script_engine.cpp
  2. 1 0
      src/runner/workflow_engine.cpp
  3. 23 0
      tests/nodes/fs-readdir.json

+ 85 - 1
src/runner/engine/script_engine.cpp

@@ -2558,6 +2558,89 @@ void ScriptEngine::setupBuiltinAPIs() {
         }
     }, "stat", 1));
 
+    // fs.readdir(path) - List a directory, one level only. Recursing here would
+    // be an unbounded walk driven by whatever happens to be on disk, so a node
+    // that wants a tree walks it itself.
+    JS_SetPropertyStr(ctx, fs, "readdir", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_ThrowTypeError(ctx, "fs.readdir requires path argument");
+        }
+
+        const char* path = JS_ToCString(ctx, argv[0]);
+        if (!path) {
+            return JS_ThrowTypeError(ctx, "path must be a string");
+        }
+        std::string path_str(path);
+        JS_FreeCString(ctx, path);
+
+        try {
+            if (!std::filesystem::exists(path_str)) {
+                JSValue response = JS_NewObject(ctx);
+                JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+                JS_SetPropertyStr(ctx, response, "error",
+                                  JS_NewString(ctx, ("Directory not found: " + path_str).c_str()));
+                return response;
+            }
+            if (!std::filesystem::is_directory(path_str)) {
+                JSValue response = JS_NewObject(ctx);
+                JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+                JS_SetPropertyStr(ctx, response, "error",
+                                  JS_NewString(ctx, ("Not a directory: " + path_str).c_str()));
+                return response;
+            }
+
+            JSValue entries = JS_NewArray(ctx);
+            uint32_t index = 0;
+
+            for (const auto& entry : std::filesystem::directory_iterator(path_str)) {
+                JSValue item = JS_NewObject(ctx);
+                JS_SetPropertyStr(ctx, item, "name",
+                                  JS_NewString(ctx, entry.path().filename().string().c_str()));
+                JS_SetPropertyStr(ctx, item, "path",
+                                  JS_NewString(ctx, entry.path().string().c_str()));
+
+                const bool is_dir = entry.is_directory();
+                JS_SetPropertyStr(ctx, item, "isDirectory", is_dir ? JS_TRUE : JS_FALSE);
+
+                int64_t size = 0;
+                int64_t mtime_ms = 0;
+                // A file removed between listing and stat is ordinary on a
+                // directory being written to, and is reported with zeroes
+                // rather than failing the whole listing.
+                try {
+                    if (entry.is_regular_file()) {
+                        size = static_cast<int64_t>(entry.file_size());
+                    }
+                    auto mtime = entry.last_write_time();
+                    // file_time_type's epoch is not guaranteed to match
+                    // system_clock's (it does not on this libstdc++), so this
+                    // has to go through clock_cast rather than
+                    // time_since_epoch() directly, or modifiedAt comes out
+                    // shifted by whatever offset the filesystem clock uses.
+                    auto sys_time = std::chrono::clock_cast<std::chrono::system_clock>(mtime);
+                    mtime_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+                        sys_time.time_since_epoch()).count();
+                } catch (const std::exception&) {
+                }
+
+                JS_SetPropertyStr(ctx, item, "size", JS_NewInt64(ctx, size));
+                JS_SetPropertyStr(ctx, item, "modifiedAt", JS_NewInt64(ctx, mtime_ms));
+
+                JS_SetPropertyUint32(ctx, entries, index++, item);
+            }
+
+            JSValue response = JS_NewObject(ctx);
+            JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
+            JS_SetPropertyStr(ctx, response, "entries", entries);
+            return response;
+        } catch (const std::exception& e) {
+            JSValue response = JS_NewObject(ctx);
+            JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+            JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, e.what()));
+            return response;
+        }
+    }, "readdir", 1));
+
     JS_SetPropertyStr(ctx, smartbotic, "fs", fs);
 
     // smartbotic.process - Process execution API for external commands
@@ -4392,7 +4475,8 @@ ScriptResult ScriptEngine::execute(const std::string& script, const ScriptContex
     // Create context object for the execute function
     nlohmann::json ctx_json = {
         {"executionId", context.execution_id},
-        {"nodeId", context.node_id}
+        {"nodeId", context.node_id},
+        {"workflowId", context.workflow_id}
     };
     std::string ctx_str = ctx_json.dump();
     JSValue ctx_val = JS_ParseJSON(context_, ctx_str.c_str(), ctx_str.size(), "<context>");

+ 1 - 0
src/runner/workflow_engine.cpp

@@ -902,6 +902,7 @@ NodeExecutionResult WorkflowEngine::executeNode(const WorkflowNode& node,
     engine::ScriptContext ctx;
     ctx.execution_id = execution_id;
     ctx.node_id = node.id;
+    ctx.workflow_id = workflow.id;
     ctx.input = input;
     ctx.config = node.config;
     ctx.log_handler = [&node](const std::string& level, const std::string& msg) {

+ 23 - 0
tests/nodes/fs-readdir.json

@@ -0,0 +1,23 @@
+{
+  "name": "verify-fs-readdir",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "n2", "name": "List", "type": "code", "position": {"x": 0, "y": 100},
+     "config": {"code": "const dir = '/tmp/sb-readdir-test';\nsmartbotic.fs.mkdir(dir);\nsmartbotic.fs.writeFile(dir + '/one.txt', smartbotic.utils.base64Encode('hello'));\nsmartbotic.fs.writeFile(dir + '/two.txt', smartbotic.utils.base64Encode('worldwide'));\nsmartbotic.fs.mkdir(dir + '/sub');\n\nconst listing = smartbotic.fs.readdir(dir);\nconst missing = smartbotic.fs.readdir('/tmp/sb-readdir-does-not-exist');\nconst notDir = smartbotic.fs.readdir(dir + '/one.txt');\n\nconst names = listing.entries.map(function (e) { return e.name; }).sort();\nconst one = listing.entries.filter(function (e) { return e.name === 'one.txt'; })[0];\nconst sub = listing.entries.filter(function (e) { return e.name === 'sub'; })[0];\n\nsmartbotic.fs.unlink(dir + '/one.txt');\nsmartbotic.fs.unlink(dir + '/two.txt');\n\nreturn {\n    ok: listing.success,\n    names: names,\n    oneSize: one.size,\n    oneHasMtime: one.modifiedAt > 1600000000000,\n    subIsDirectory: sub.isDirectory,\n    oneIsDirectory: one.isDirectory,\n    missingRejected: missing.success === false,\n    notDirRejected: notDir.success === false,\n    hasWorkflowId: typeof context.workflowId === 'string' && context.workflowId.length > 0\n};"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "n2", "targetInput": "data"}
+  ],
+  "expect": {
+    "n2": {"status": "completed", "output": {"result": {
+      "ok": true,
+      "names": ["one.txt", "sub", "two.txt"],
+      "oneSize": 5,
+      "oneHasMtime": true,
+      "subIsDirectory": true,
+      "oneIsDirectory": false,
+      "missingRejected": true,
+      "notDirRejected": true
+    }}}
+  }
+}