diff --git a/examples/server/server.js b/examples/server/server.js index adc34ff..6464598 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -1,28 +1,22 @@ -import { html } from './html'; import meow from './meow.txt'; + +import { html } from './html'; +import { readFile } from 'ant:fs'; import { Radix3 } from './radix3'; const router = new Radix3(); -router.insert('/', c => { - return c.res.body(`Welcome to Ant HTTP Server with Radix3 Router! - -Available routes: - GET / - GET /meow - GET /hello - GET /status - GET /users/:id - GET /users/:id/posts - GET /files/*path - GET /api/v1/users - GET /api/v2/demo`); -}); +router.insert('/', c => c.res.body(`Welcome to Ant ${Ant.version}!`)); router.insert('/meow', async c => { return c.res.body(meow); }); +router.insert('/fs/meow', async c => { + const file = await readFile('./meow.txt'); + return c.res.body(file); +}); + router.insert('/hello', async c => { return c.res.body('Hello, World!'); }); diff --git a/include/modules/fs.h b/include/modules/fs.h new file mode 100644 index 0000000..d672544 --- /dev/null +++ b/include/modules/fs.h @@ -0,0 +1,10 @@ +#ifndef ANT_FS_MODULE_H +#define ANT_FS_MODULE_H + +#include "ant.h" + +jsval_t fs_library(struct js *js); +void fs_poll_events(void); +int has_pending_fs_ops(void); + +#endif diff --git a/meson.build b/meson.build index d1ac9c6..47fe425 100644 --- a/meson.build +++ b/meson.build @@ -67,7 +67,7 @@ endif build_date = run_command('date', '+%Y-%m-%d', check: true).stdout().strip() version_conf = configuration_data() -version_conf.set('ANT_VERSION', '0.0.6.29') +version_conf.set('ANT_VERSION', '0.0.6.30') version_conf.set('ANT_GIT_HASH', git_hash) version_conf.set('ANT_BUILD_DATE', build_date) diff --git a/src/ant.c b/src/ant.c index ef6d035..8635573 100644 --- a/src/ant.c +++ b/src/ant.c @@ -17,6 +17,8 @@ #include "ant.h" #include "config.h" + +#include "modules/fs.h" #include "modules/timer.h" #include "modules/fetch.h" @@ -429,8 +431,9 @@ static bool has_ready_coroutines(void) { void js_poll_events(struct js *js) { fetch_poll_events(); - int has_timers = has_pending_timers(); + fs_poll_events(); + int has_timers = has_pending_timers(); if (has_timers) { int64_t next_timeout_ms = get_next_timer_timeout(); if (next_timeout_ms <= 0) process_timers(js); @@ -481,7 +484,7 @@ void js_poll_events(struct js *js) { } void js_run_event_loop(struct js *js) { - while (has_pending_microtasks() || has_pending_timers() || has_pending_coroutines() || has_pending_fetches()) { + while (has_pending_microtasks() || has_pending_timers() || has_pending_coroutines() || has_pending_fetches() || has_pending_fs_ops()) { js_poll_events(js); if (!has_pending_microtasks() && has_pending_timers() && !has_ready_coroutines()) { @@ -489,7 +492,7 @@ void js_run_event_loop(struct js *js) { if (next_timeout_ms > 0) usleep(next_timeout_ms > 1000000 ? 1000000 : next_timeout_ms * 1000); } - if (!has_pending_microtasks() && !has_pending_timers() && !has_pending_coroutines() && !has_pending_fetches()) break; + if (!has_pending_microtasks() && !has_pending_timers() && !has_pending_coroutines() && !has_pending_fetches() && !has_pending_fs_ops()) break; } js_poll_events(js); diff --git a/src/main.c b/src/main.c index 35e6f87..74a4669 100644 --- a/src/main.c +++ b/src/main.c @@ -2,7 +2,6 @@ #include #include #include -#include #include #include @@ -13,6 +12,7 @@ #include "modules/builtin.h" #include "modules/io.h" +#include "modules/fs.h" #include "modules/crypto.h" #include "modules/server.h" #include "modules/timer.h" @@ -134,8 +134,9 @@ int main(int argc, char *argv[]) { init_timer_module(); init_process_module(); - ant_register_library("ant/shell", shell_library); - + ant_register_library("ant:fs", fs_library); + ant_register_library("ant:shell", shell_library); + if (repl_mode) ant_repl_run(); else { js_result = execute_module(js, module_file); js_run_event_loop(js); diff --git a/src/modules/fs.c b/src/modules/fs.c new file mode 100644 index 0000000..ee87611 --- /dev/null +++ b/src/modules/fs.c @@ -0,0 +1,529 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "modules/fs.h" +#include "runtime.h" + +typedef enum { + FS_OP_READ, + FS_OP_WRITE, + FS_OP_UNLINK, + FS_OP_MKDIR, + FS_OP_RMDIR, + FS_OP_STAT +} fs_op_type_t; + +typedef struct fs_request_s { + struct js *js; + jsval_t promise; + uv_fs_t uv_req; + fs_op_type_t op_type; + char *path; + char *data; + size_t data_len; + uv_file fd; + int completed; + int failed; + char *error_msg; +} fs_request_t; + +static uv_loop_t *fs_loop = NULL; +static UT_array *pending_requests = NULL; + +static void free_fs_request(fs_request_t *req) { + if (!req) return; + + if (req->path) free(req->path); + if (req->data) free(req->data); + if (req->error_msg) free(req->error_msg); + + uv_fs_req_cleanup(&req->uv_req); + free(req); +} + +static void remove_pending_request(fs_request_t *req) { + if (!req || !pending_requests) return; + + fs_request_t **p = NULL; + unsigned int i = 0; + + while ((p = (fs_request_t**)utarray_next(pending_requests, p))) { + if (*p == req) { + utarray_erase(pending_requests, i, 1); + break; + } + i++; + } +} + +static void complete_request(fs_request_t *req) { + if (req->failed) { + const char *err_msg = req->error_msg ? req->error_msg : "Unknown error"; + jsval_t err = js_mkstr(req->js, err_msg, strlen(err_msg)); + js_reject_promise(req->js, req->promise, err); + } else { + jsval_t result; + if (req->op_type == FS_OP_READ && req->data) { + result = js_mkstr(req->js, req->data, req->data_len); + } else if (req->op_type == FS_OP_STAT) { + result = js_mkundef(); + } else { + result = js_mkundef(); + } + js_resolve_promise(req->js, req->promise, result); + } + + remove_pending_request(req); + free_fs_request(req); +} + +static void on_read_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(uv_req->result)); + req->completed = 1; + complete_request(req); + return; + } + + req->data_len = uv_req->result; + + uv_fs_t close_req; + uv_fs_close(fs_loop, &close_req, req->fd, NULL); + uv_fs_req_cleanup(&close_req); + + req->completed = 1; + complete_request(req); +} + +static void on_open_for_read(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(uv_req->result)); + req->completed = 1; + complete_request(req); + return; + } + + req->fd = uv_req->result; + uv_fs_req_cleanup(uv_req); + + uv_fs_t stat_req; + int stat_result = uv_fs_fstat(fs_loop, &stat_req, req->fd, NULL); + + if (stat_result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(stat_result)); + req->completed = 1; + uv_fs_t close_req; + uv_fs_close(fs_loop, &close_req, req->fd, NULL); + uv_fs_req_cleanup(&close_req); + complete_request(req); + return; + } + + size_t file_size = stat_req.statbuf.st_size; + uv_fs_req_cleanup(&stat_req); + + req->data = malloc(file_size + 1); + if (!req->data) { + req->failed = 1; + req->error_msg = strdup("Out of memory"); + req->completed = 1; + uv_fs_t close_req; + uv_fs_close(fs_loop, &close_req, req->fd, NULL); + uv_fs_req_cleanup(&close_req); + complete_request(req); + return; + } + + uv_buf_t buf = uv_buf_init(req->data, file_size); + int read_result = uv_fs_read(fs_loop, uv_req, req->fd, &buf, 1, 0, on_read_complete); + + if (read_result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(read_result)); + req->completed = 1; + uv_fs_t close_req; + uv_fs_close(fs_loop, &close_req, req->fd, NULL); + uv_fs_req_cleanup(&close_req); + complete_request(req); + return; + } +} + +static void on_write_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(uv_req->result)); + } + + uv_fs_t close_req; + uv_fs_close(fs_loop, &close_req, req->fd, NULL); + uv_fs_req_cleanup(&close_req); + + req->completed = 1; + complete_request(req); +} + +static void on_open_for_write(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(uv_req->result)); + req->completed = 1; + complete_request(req); + return; + } + + req->fd = uv_req->result; + uv_fs_req_cleanup(uv_req); + + uv_buf_t buf = uv_buf_init(req->data, req->data_len); + int write_result = uv_fs_write(fs_loop, uv_req, req->fd, &buf, 1, 0, on_write_complete); + + if (write_result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(write_result)); + req->completed = 1; + uv_fs_t close_req; + uv_fs_close(fs_loop, &close_req, req->fd, NULL); + uv_fs_req_cleanup(&close_req); + complete_request(req); + return; + } +} + +static void on_unlink_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(uv_req->result)); + } + + req->completed = 1; + complete_request(req); +} + +static void on_mkdir_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(uv_req->result)); + } + + req->completed = 1; + complete_request(req); +} + +static void on_rmdir_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(uv_req->result)); + } + + req->completed = 1; + complete_request(req); +} + +static void on_stat_complete(uv_fs_t *uv_req) { + fs_request_t *req = (fs_request_t *)uv_req->data; + + if (uv_req->result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(uv_req->result)); + req->completed = 1; + complete_request(req); + return; + } + + jsval_t stat_obj = js_mkobj(req->js); + js_set(req->js, stat_obj, "size", js_mknum((double)uv_req->statbuf.st_size)); + js_set(req->js, stat_obj, "mode", js_mknum((double)uv_req->statbuf.st_mode)); + js_set(req->js, stat_obj, "isFile", S_ISREG(uv_req->statbuf.st_mode) ? js_mktrue() : js_mkfalse()); + js_set(req->js, stat_obj, "isDirectory", S_ISDIR(uv_req->statbuf.st_mode) ? js_mktrue() : js_mkfalse()); + + req->completed = 1; + js_resolve_promise(req->js, req->promise, stat_obj); + remove_pending_request(req); + free_fs_request(req); +} + +static void ensure_fs_loop(void) { + if (!fs_loop) { + if (rt->external_event_loop_active) { + fs_loop = uv_default_loop(); + } else { + fs_loop = malloc(sizeof(uv_loop_t)); + uv_loop_init(fs_loop); + } + } + + if (!pending_requests) { + utarray_new(pending_requests, &ut_ptr_icd); + } +} + +static jsval_t builtin_fs_readFile(struct js *js, jsval_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "readFile() requires a path argument"); + + if (js_type(args[0]) != JS_STR) return js_mkerr(js, "readFile() path must be a string"); + + size_t path_len; + char *path = js_getstr(js, args[0], &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + ensure_fs_loop(); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_READ; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + utarray_push_back(pending_requests, &req); + + int result = uv_fs_open(fs_loop, &req->uv_req, req->path, O_RDONLY, 0, on_open_for_read); + + if (result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(result)); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + +static jsval_t builtin_fs_writeFile(struct js *js, jsval_t *args, int nargs) { + if (nargs < 2) return js_mkerr(js, "writeFile() requires path and data arguments"); + + if (js_type(args[0]) != JS_STR) return js_mkerr(js, "writeFile() path must be a string"); + if (js_type(args[1]) != JS_STR) return js_mkerr(js, "writeFile() data must be a string"); + + size_t path_len, data_len; + char *path = js_getstr(js, args[0], &path_len); + char *data = js_getstr(js, args[1], &data_len); + + if (!path || !data) return js_mkerr(js, "Failed to get arguments"); + + ensure_fs_loop(); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_WRITE; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->data = malloc(data_len); + if (!req->data) { + free(req->path); + free(req); + return js_mkerr(js, "Out of memory"); + } + + memcpy(req->data, data, data_len); + req->data_len = data_len; + req->uv_req.data = req; + + utarray_push_back(pending_requests, &req); + + int result = uv_fs_open(fs_loop, &req->uv_req, req->path, + O_WRONLY | O_CREAT | O_TRUNC, 0644, on_open_for_write); + + if (result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(result)); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + +static jsval_t builtin_fs_unlink(struct js *js, jsval_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "unlink() requires a path argument"); + + if (js_type(args[0]) != JS_STR) return js_mkerr(js, "unlink() path must be a string"); + + size_t path_len; + char *path = js_getstr(js, args[0], &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + ensure_fs_loop(); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_UNLINK; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + utarray_push_back(pending_requests, &req); + + int result = uv_fs_unlink(fs_loop, &req->uv_req, req->path, on_unlink_complete); + + if (result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(result)); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + +static jsval_t builtin_fs_mkdir(struct js *js, jsval_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "mkdir() requires a path argument"); + + if (js_type(args[0]) != JS_STR) return js_mkerr(js, "mkdir() path must be a string"); + + size_t path_len; + char *path = js_getstr(js, args[0], &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + int mode = 0755; + if (nargs >= 2 && js_type(args[1]) == JS_NUM) { + mode = (int)js_getnum(args[1]); + } + + ensure_fs_loop(); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_MKDIR; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + utarray_push_back(pending_requests, &req); + + int result = uv_fs_mkdir(fs_loop, &req->uv_req, req->path, mode, on_mkdir_complete); + + if (result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(result)); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + +static jsval_t builtin_fs_rmdir(struct js *js, jsval_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "rmdir() requires a path argument"); + + if (js_type(args[0]) != JS_STR) return js_mkerr(js, "rmdir() path must be a string"); + + size_t path_len; + char *path = js_getstr(js, args[0], &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + ensure_fs_loop(); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_RMDIR; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + utarray_push_back(pending_requests, &req); + + int result = uv_fs_rmdir(fs_loop, &req->uv_req, req->path, on_rmdir_complete); + + if (result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(result)); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + +static jsval_t builtin_fs_stat(struct js *js, jsval_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "stat() requires a path argument"); + + if (js_type(args[0]) != JS_STR) return js_mkerr(js, "stat() path must be a string"); + + size_t path_len; + char *path = js_getstr(js, args[0], &path_len); + if (!path) return js_mkerr(js, "Failed to get path string"); + + ensure_fs_loop(); + + fs_request_t *req = calloc(1, sizeof(fs_request_t)); + if (!req) return js_mkerr(js, "Out of memory"); + + req->js = js; + req->op_type = FS_OP_STAT; + req->promise = js_mkpromise(js); + req->path = strndup(path, path_len); + req->uv_req.data = req; + + utarray_push_back(pending_requests, &req); + + int result = uv_fs_stat(fs_loop, &req->uv_req, req->path, on_stat_complete); + + if (result < 0) { + req->failed = 1; + req->error_msg = strdup(uv_strerror(result)); + req->completed = 1; + complete_request(req); + } + + return req->promise; +} + +jsval_t fs_library(struct js *js) { + jsval_t lib = js_mkobj(js); + + js_set(js, lib, "readFile", js_mkfun(builtin_fs_readFile)); + js_set(js, lib, "writeFile", js_mkfun(builtin_fs_writeFile)); + js_set(js, lib, "unlink", js_mkfun(builtin_fs_unlink)); + js_set(js, lib, "mkdir", js_mkfun(builtin_fs_mkdir)); + js_set(js, lib, "rmdir", js_mkfun(builtin_fs_rmdir)); + js_set(js, lib, "stat", js_mkfun(builtin_fs_stat)); + + return lib; +} + +int has_pending_fs_ops(void) { + return (pending_requests && utarray_len(pending_requests) > 0) || (fs_loop && uv_loop_alive(fs_loop)); +} + +void fs_poll_events(void) { + if (fs_loop && fs_loop == uv_default_loop() && rt->external_event_loop_active) return; + if (fs_loop && uv_loop_alive(fs_loop)) { + uv_run(fs_loop, UV_RUN_ONCE); + if (pending_requests && utarray_len(pending_requests) > 0) usleep(1000); + } +} diff --git a/src/modules/io.c b/src/modules/io.c index 6883baa..49c6372 100644 --- a/src/modules/io.c +++ b/src/modules/io.c @@ -3,7 +3,6 @@ #include #include #include -#include #include "runtime.h" #include "modules/io.h" diff --git a/tests/test_fs_async.js b/tests/test_fs_async.js new file mode 100644 index 0000000..f25b8c1 --- /dev/null +++ b/tests/test_fs_async.js @@ -0,0 +1,58 @@ +import { readFile, writeFile, unlink, mkdir, rmdir, stat } from 'ant:fs'; + +console.log('Testing ant:fs with async/await...\n'); + +async function testFs() { + const testDir = 'tests/.fs_test_tmp'; + const testFile = testDir + '/async_test.txt'; + const testData = 'Hello from async ant:fs!'; + + try { + // Test mkdir + console.log('=== Test: mkdir ==='); + try { + await mkdir(testDir); + console.log('✓ Directory created'); + } catch (e) { + console.log('Directory might exist, continuing...'); + } + + // Test writeFile + console.log('\n=== Test: writeFile ==='); + await writeFile(testFile, testData); + console.log('✓ File written'); + + // Test readFile + console.log('\n=== Test: readFile ==='); + const content = await readFile(testFile); + console.log('✓ File read, length:', content.length); + if (content === testData) { + console.log('✓ Content matches!'); + } else { + console.log('✗ Content mismatch!'); + } + + // Test stat + console.log('\n=== Test: stat ==='); + const stats = await stat(testFile); + console.log('✓ File stats:'); + console.log(' Size:', stats.size); + console.log(' Is file:', stats.isFile); + console.log(' Is directory:', stats.isDirectory); + + // Cleanup + console.log('\n=== Cleanup ==='); + await unlink(testFile); + console.log('✓ File deleted'); + + await rmdir(testDir); + console.log('✓ Directory deleted'); + + console.log('\n✓✓✓ All tests passed! ✓✓✓'); + } catch (error) { + console.error('\n✗ Test failed:', error); + } +} + +// Run the async function +testFs(); diff --git a/tests/test_shell.js b/tests/test_shell.js index 4a60116..574c478 100644 --- a/tests/test_shell.js +++ b/tests/test_shell.js @@ -1,4 +1,4 @@ -import { $ } from 'ant/shell'; +import { $ } from 'ant:shell'; console.log('Testing $ shell command execution...');