|
|
@@ -10,6 +10,8 @@
|
|
|
#include <iomanip>
|
|
|
#include <algorithm>
|
|
|
#include <regex>
|
|
|
+#include <fstream>
|
|
|
+#include <filesystem>
|
|
|
#include <openssl/sha.h>
|
|
|
#include <openssl/evp.h>
|
|
|
#include <openssl/buffer.h>
|
|
|
@@ -303,6 +305,104 @@ static JSValue js_http_request(JSContext* ctx, JSValue this_val, int argc, JSVal
|
|
|
}
|
|
|
JS_FreeValue(ctx, body_val);
|
|
|
|
|
|
+ // Check for multipart file upload (files array with {name, filename, data, mimeType})
|
|
|
+ JSValue files_val = JS_GetPropertyStr(ctx, options, "files");
|
|
|
+ if (JS_IsArray(ctx, files_val)) {
|
|
|
+ // Build multipart/form-data body with binary file support
|
|
|
+ std::string boundary = "----SmartBoticBoundary" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
|
|
|
+ headers["Content-Type"] = "multipart/form-data; boundary=" + boundary;
|
|
|
+
|
|
|
+ body.clear();
|
|
|
+ std::string crlf = "\r\n";
|
|
|
+
|
|
|
+ // First add any form fields from formData option
|
|
|
+ JSValue form_data_val = JS_GetPropertyStr(ctx, options, "formData");
|
|
|
+ if (JS_IsObject(form_data_val)) {
|
|
|
+ JSPropertyEnum* props;
|
|
|
+ uint32_t prop_count;
|
|
|
+ JS_GetOwnPropertyNames(ctx, &props, &prop_count, form_data_val, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY);
|
|
|
+ for (uint32_t i = 0; i < prop_count; i++) {
|
|
|
+ const char* key = JS_AtomToCString(ctx, props[i].atom);
|
|
|
+ JSValue val = JS_GetProperty(ctx, form_data_val, props[i].atom);
|
|
|
+ const char* val_str = JS_ToCString(ctx, val);
|
|
|
+ if (key && val_str) {
|
|
|
+ body += "--" + boundary + crlf;
|
|
|
+ body += "Content-Disposition: form-data; name=\"" + std::string(key) + "\"" + crlf;
|
|
|
+ body += crlf;
|
|
|
+ body += val_str;
|
|
|
+ body += crlf;
|
|
|
+ }
|
|
|
+ if (key) JS_FreeCString(ctx, key);
|
|
|
+ if (val_str) JS_FreeCString(ctx, val_str);
|
|
|
+ JS_FreeValue(ctx, val);
|
|
|
+ JS_FreeAtom(ctx, props[i].atom);
|
|
|
+ }
|
|
|
+ js_free(ctx, props);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, form_data_val);
|
|
|
+
|
|
|
+ // Add files
|
|
|
+ int64_t files_len = 0;
|
|
|
+ JSValue len_val = JS_GetPropertyStr(ctx, files_val, "length");
|
|
|
+ JS_ToInt64(ctx, &files_len, len_val);
|
|
|
+ JS_FreeValue(ctx, len_val);
|
|
|
+
|
|
|
+ for (int64_t i = 0; i < files_len; i++) {
|
|
|
+ JSValue file_obj = JS_GetPropertyUint32(ctx, files_val, i);
|
|
|
+ if (JS_IsObject(file_obj)) {
|
|
|
+ // Get file properties
|
|
|
+ std::string field_name = "file";
|
|
|
+ std::string filename = "file";
|
|
|
+ std::string mime_type = "application/octet-stream";
|
|
|
+ std::string file_data;
|
|
|
+
|
|
|
+ JSValue name_val = JS_GetPropertyStr(ctx, file_obj, "name");
|
|
|
+ if (JS_IsString(name_val)) {
|
|
|
+ const char* s = JS_ToCString(ctx, name_val);
|
|
|
+ if (s) { field_name = s; JS_FreeCString(ctx, s); }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, name_val);
|
|
|
+
|
|
|
+ JSValue filename_val = JS_GetPropertyStr(ctx, file_obj, "filename");
|
|
|
+ if (JS_IsString(filename_val)) {
|
|
|
+ const char* s = JS_ToCString(ctx, filename_val);
|
|
|
+ if (s) { filename = s; JS_FreeCString(ctx, s); }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, filename_val);
|
|
|
+
|
|
|
+ JSValue mime_val = JS_GetPropertyStr(ctx, file_obj, "mimeType");
|
|
|
+ if (JS_IsString(mime_val)) {
|
|
|
+ const char* s = JS_ToCString(ctx, mime_val);
|
|
|
+ if (s) { mime_type = s; JS_FreeCString(ctx, s); }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, mime_val);
|
|
|
+
|
|
|
+ JSValue data_val = JS_GetPropertyStr(ctx, file_obj, "data");
|
|
|
+ if (JS_IsString(data_val)) {
|
|
|
+ const char* s = JS_ToCString(ctx, data_val);
|
|
|
+ if (s) {
|
|
|
+ // Decode base64 to binary
|
|
|
+ file_data = base64Decode(s);
|
|
|
+ JS_FreeCString(ctx, s);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, data_val);
|
|
|
+
|
|
|
+ // Build file part with binary data
|
|
|
+ body += "--" + boundary + crlf;
|
|
|
+ body += "Content-Disposition: form-data; name=\"" + field_name + "\"; filename=\"" + filename + "\"" + crlf;
|
|
|
+ body += "Content-Type: " + mime_type + crlf;
|
|
|
+ body += crlf;
|
|
|
+ body.append(file_data); // Binary data
|
|
|
+ body += crlf;
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, file_obj);
|
|
|
+ }
|
|
|
+
|
|
|
+ body += "--" + boundary + "--" + crlf;
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, files_val);
|
|
|
+
|
|
|
// Get timeout (default 30000ms)
|
|
|
long timeout_ms = 30000;
|
|
|
JSValue timeout_val = JS_GetPropertyStr(ctx, options, "timeout");
|
|
|
@@ -887,6 +987,263 @@ void ScriptEngine::setupBuiltinAPIs() {
|
|
|
|
|
|
JS_SetPropertyStr(ctx, smartbotic, "http", http);
|
|
|
|
|
|
+ // smartbotic.fs - Filesystem API for binary storage
|
|
|
+ JSValue fs = JS_NewObject(ctx);
|
|
|
+
|
|
|
+ // fs.writeFile(path, data, encoding) - Write data to file
|
|
|
+ // encoding: 'base64' (default) or 'utf8'
|
|
|
+ JS_SetPropertyStr(ctx, fs, "writeFile", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 2) {
|
|
|
+ return JS_ThrowTypeError(ctx, "fs.writeFile requires path and data arguments");
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+
|
|
|
+ const char* data = JS_ToCString(ctx, argv[1]);
|
|
|
+ if (!data) {
|
|
|
+ return JS_ThrowTypeError(ctx, "data must be a string");
|
|
|
+ }
|
|
|
+ std::string data_str(data);
|
|
|
+ JS_FreeCString(ctx, data);
|
|
|
+
|
|
|
+ // Get encoding (default: base64)
|
|
|
+ std::string encoding = "base64";
|
|
|
+ if (argc >= 3 && !JS_IsUndefined(argv[2])) {
|
|
|
+ const char* enc = JS_ToCString(ctx, argv[2]);
|
|
|
+ if (enc) {
|
|
|
+ encoding = enc;
|
|
|
+ JS_FreeCString(ctx, enc);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Create parent directories if needed
|
|
|
+ std::filesystem::path file_path(path_str);
|
|
|
+ if (file_path.has_parent_path()) {
|
|
|
+ std::filesystem::create_directories(file_path.parent_path());
|
|
|
+ }
|
|
|
+
|
|
|
+ std::ofstream file(path_str, std::ios::binary);
|
|
|
+ if (!file) {
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, ("Failed to open file for writing: " + path_str).c_str()));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (encoding == "base64") {
|
|
|
+ // Decode base64 and write binary
|
|
|
+ std::string decoded = base64Decode(data_str);
|
|
|
+ file.write(decoded.data(), decoded.size());
|
|
|
+ } else {
|
|
|
+ // Write as-is (utf8)
|
|
|
+ file.write(data_str.data(), data_str.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ file.close();
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "path", JS_NewString(ctx, path_str.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, response, "size", JS_NewInt64(ctx, std::filesystem::file_size(path_str)));
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }, "writeFile", 3));
|
|
|
+
|
|
|
+ // fs.readFile(path, encoding) - Read file data
|
|
|
+ // encoding: 'base64' (default) or 'utf8'
|
|
|
+ JS_SetPropertyStr(ctx, fs, "readFile", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "fs.readFile 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);
|
|
|
+
|
|
|
+ std::string encoding = "base64";
|
|
|
+ if (argc >= 2 && !JS_IsUndefined(argv[1])) {
|
|
|
+ const char* enc = JS_ToCString(ctx, argv[1]);
|
|
|
+ if (enc) {
|
|
|
+ encoding = enc;
|
|
|
+ JS_FreeCString(ctx, enc);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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, ("File not found: " + path_str).c_str()));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::ifstream file(path_str, std::ios::binary);
|
|
|
+ if (!file) {
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, ("Failed to open file: " + path_str).c_str()));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::ostringstream ss;
|
|
|
+ ss << file.rdbuf();
|
|
|
+ std::string content = ss.str();
|
|
|
+ file.close();
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "size", JS_NewInt64(ctx, content.size()));
|
|
|
+
|
|
|
+ if (encoding == "base64") {
|
|
|
+ std::string encoded = base64Encode(content);
|
|
|
+ JS_SetPropertyStr(ctx, response, "data", JS_NewString(ctx, encoded.c_str()));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "data", JS_NewString(ctx, content.c_str()));
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }, "readFile", 2));
|
|
|
+
|
|
|
+ // fs.exists(path) - Check if file exists
|
|
|
+ JS_SetPropertyStr(ctx, fs, "exists", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "fs.exists requires path argument");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* path = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!path) {
|
|
|
+ return JS_FALSE;
|
|
|
+ }
|
|
|
+ std::string path_str(path);
|
|
|
+ JS_FreeCString(ctx, path);
|
|
|
+
|
|
|
+ return std::filesystem::exists(path_str) ? JS_TRUE : JS_FALSE;
|
|
|
+ }, "exists", 1));
|
|
|
+
|
|
|
+ // fs.mkdir(path) - Create directory (recursive)
|
|
|
+ JS_SetPropertyStr(ctx, fs, "mkdir", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "fs.mkdir 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 {
|
|
|
+ std::filesystem::create_directories(path_str);
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "path", JS_NewString(ctx, path_str.c_str()));
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }, "mkdir", 1));
|
|
|
+
|
|
|
+ // fs.unlink(path) - Delete file
|
|
|
+ JS_SetPropertyStr(ctx, fs, "unlink", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "fs.unlink 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, ("File not found: " + path_str).c_str()));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+ std::filesystem::remove(path_str);
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }, "unlink", 1));
|
|
|
+
|
|
|
+ // fs.stat(path) - Get file info
|
|
|
+ JS_SetPropertyStr(ctx, fs, "stat", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "fs.stat 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, ("File not found: " + path_str).c_str()));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto status = std::filesystem::status(path_str);
|
|
|
+ auto size = std::filesystem::is_regular_file(path_str) ? std::filesystem::file_size(path_str) : 0;
|
|
|
+ auto mtime = std::filesystem::last_write_time(path_str);
|
|
|
+ auto mtime_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
+ mtime.time_since_epoch()
|
|
|
+ ).count();
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "size", JS_NewInt64(ctx, size));
|
|
|
+ JS_SetPropertyStr(ctx, response, "mtime", JS_NewInt64(ctx, mtime_ms));
|
|
|
+ JS_SetPropertyStr(ctx, response, "isFile", std::filesystem::is_regular_file(path_str) ? JS_TRUE : JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "isDirectory", std::filesystem::is_directory(path_str) ? JS_TRUE : JS_FALSE);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }, "stat", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, smartbotic, "fs", fs);
|
|
|
+
|
|
|
JS_SetPropertyStr(ctx, global, "smartbotic", smartbotic);
|
|
|
|
|
|
// Console for compatibility
|
|
|
@@ -1570,6 +1927,14 @@ static JSValue js_imap_fetch(JSContext* ctx, JSValue this_val, int argc, JSValue
|
|
|
}
|
|
|
JS_FreeValue(ctx, mailbox_val);
|
|
|
|
|
|
+ // Get peek option (default false) - if true, don't mark as read
|
|
|
+ bool peek = false;
|
|
|
+ JSValue peek_val = JS_GetPropertyStr(ctx, options, "peek");
|
|
|
+ if (!JS_IsUndefined(peek_val) && JS_IsBool(peek_val)) {
|
|
|
+ peek = JS_ToBool(ctx, peek_val);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, peek_val);
|
|
|
+
|
|
|
// Get credentials
|
|
|
auto cred_result = script_ctx->credentials_get_imap(credential_id);
|
|
|
if (cred_result.failed()) {
|
|
|
@@ -1583,7 +1948,7 @@ static JSValue js_imap_fetch(JSContext* ctx, JSValue this_val, int argc, JSValue
|
|
|
imap::ImapCredentials imap_creds{cred.host, cred.port, cred.username, cred.password, cred.use_ssl};
|
|
|
imap::ImapClient client(imap_creds);
|
|
|
|
|
|
- auto fetch_result = client.fetch(mailbox, uid);
|
|
|
+ auto fetch_result = client.fetch(mailbox, uid, peek);
|
|
|
if (fetch_result.failed()) {
|
|
|
return JS_ThrowInternalError(ctx, "IMAP fetch failed: %s",
|
|
|
fetch_result.error().message().c_str());
|