diff options
author | James Prestwood <james.prestwood@intel.com> | 2017-03-08 15:18:19 -0800 |
---|---|---|
committer | Geoff Gustafson <geoff@linux.intel.com> | 2017-03-08 15:18:19 -0800 |
commit | 6b7abbd61cd4105c2a278492fede1e8df0a7c288 (patch) | |
tree | 61637fbf91afc49fde1c258741b4a64e6af6a3ac | |
parent | 70a30cc653ee6792b4de44f29e78d4bed6b6dfbb (diff) |
[fs] File System module (#510)
Signed-off-by: James Prestwood <james.prestwood@intel.com>
-rw-r--r-- | docs/API.md | 2 | ||||
-rw-r--r-- | docs/fs.md | 175 | ||||
-rw-r--r-- | samples/FsAsync.js | 44 | ||||
-rw-r--r-- | samples/FsSync.js | 37 | ||||
-rwxr-xr-x | scripts/analyze.sh | 17 | ||||
-rw-r--r-- | src/Makefile.base | 1 | ||||
-rw-r--r-- | src/zjs_fs.c | 1099 | ||||
-rw-r--r-- | src/zjs_fs.h | 8 | ||||
-rw-r--r-- | src/zjs_modules.c | 4 | ||||
-rw-r--r-- | tests/test-fs.js | 195 |
10 files changed, 1582 insertions, 0 deletions
diff --git a/docs/API.md b/docs/API.md index 0d89e17..07de10e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -45,6 +45,8 @@ General [Events](./events.md) +[File System](./fs.md) + [Performance](./performance.md) [Timers](./timers.md) diff --git a/docs/fs.md b/docs/fs.md new file mode 100644 index 0000000..b6564a9 --- /dev/null +++ b/docs/fs.md @@ -0,0 +1,175 @@ +ZJS API for File System +================== + +* [Introduction](#introduction) +* [API Documentation](#api-documentation) +* [Sample Apps](#sample-apps) + +Introduction +------------ +ZJS provides File System API's which match Node.js' FS module. We describe them +here as there could potentially be minor differences. It should be noted that by +default the FS module only contains the synchronous Node.js API's. The +asynchronous API's can be compiled in by enabling the pre-processor define +`ZJS_FS_ASYNC_APIS`. They are compiled out because all of Zephyr's File +System API's are synchronous, so making the JavaScript API's asynchronous was +only adding ROM space. + +On the Arduino 101 the flash file system uses SPI and the pins are shared with +IO10-13. For this reason you will not be able to use these GPIO pins at the +same time as the file system. + +Available file modes: + +`'r'` - Open file for only reading. An error will be thrown if the file does +not exist. + +`'r+'` - Open a file for reading and writing. An error will be thrown if the +file does not exist. + +`'w'` - Opens a file for writing. The file will be overwritten if it already +exists. + +`'w+'` - Opens a file for writing and reading. The file will be overwritten if +it already exists. + +`'a'` - Opens a file for appending. The write pointer will always seek +to the end of the file during a write. + +`'a+'` - Opens a file for appending and reading. As with `'a'` the write +pointer will seek to the end for writes, but reads can be done from the +start of the file (read pointer saved across different read calls). + +Web IDL +------- +This IDL provides an overview of the interface; see below for documentation of +specific API functions. + +```javascript +// require returns a FS object +// var fs = require('fs'); + +interface Stat { + boolean isFile(); + boolean isDirectory(); +}; +``` + +API Documentation +----------------- + +### FS.openSync +`object openSync(string path, string mode);` + +Opens a file. + +`path` is the name/path of the file to open. + +`mode` is the mode to open the file in (r/w/a/r+/w+/a+). + +Returns an object representing the file descriptor. + +### FS.closeSync +`void closeSync(object fd);` + +Closes a file. + +`fd` is the descriptor returned from `openSync()`. + +### FS.unlinkSync +`void unlinkSync(string path);` + +Unlink (remove) a file from the file system. + +`path` is the file to remove. + +### FS.rmdirSync +`void rmdirSync(string path);` + +Remove a directory from the file system. + +`path` is the name of the directory. + +### FS.writeSync +`number writeSync(object fd, [String|Buffer] data, number offset, number length, optional number position);` + +Write bytes to an opened file. + +`fd` is the file descriptor returned from `openSync()`. + +`data` is either a string or buffer to write. + +`offset` is the position in `data` to start writing from. + +`length` is the number of bytes to write from `data`. + +`position` is the offset from the beginning of the file where `data` should be +written. Default is 0. + +Returns the number of bytes actually written (this may be different from `length`). + +### FS.readSync +`number readSync(object fd, buffer data, number offset, number length, number position);` + +Read bytes from a file. + +`fd` is the file descriptor returned from `openSync()`. + +`data` is a buffer where the data will be read into. + +`offset` is the offset in `data` to start writing at. + +`length` is the number of bytes to read. + +`position` is the position in the file to start reading from. + +Returns the number of bytes actually read. This may be different from `length` +if there was a read error or if the file had no more data left to read. + +### FS.truncateSync +`void truncateSync(string path, number length);` + +Truncate a file. If the length passed in is shorter than the existing file +length then the trailing file data will be lost. + +`path` is the name of the file. + +`length` is the new length of the file. + +### FS.mkdirSync +`void mkdirSync(string path)` + +Create a directory. If the directory already exists there is no effect. + +`path` is the name of the directory. + +### FS.readdirSync +`string[] readdirSync(string path);` + +Read the contents of a directory. + +`path` directory path to read. + +Returns an array of filenames and directories found in `path`. + +### FS.statSync +`Stat statSync(string path);` + +Get stats about a file or directory. + +`path` name of file or directory. + +Returns a `Stat` object for that file or directory. + +### FS.writeFileSync +`void writeFileSync(string file, [string|buffer] data);` + +Open and write data to a file. This will replace the file if it already exists. + +`file` is the name of the file to write to. + +`data` is the bytes to write into the file. + +Sample Apps +----------- +* [FS test](../tests/test-fs.js) diff --git a/samples/FsAsync.js b/samples/FsAsync.js new file mode 100644 index 0000000..75ac962 --- /dev/null +++ b/samples/FsAsync.js @@ -0,0 +1,44 @@ +var fs = require('fs'); + +console.log('File System sample'); + +fs.open('testfile.txt', 'w+', function(err, fd) { + console.log('opened file error=' + err); + fs.stat('testfile.txt', function(err, stats) { + if (stats.isFile()) { + console.log('fd is file'); + } else if (stats.isDirectory()) { + console.log('fd is directory'); + } else { + console.log('fd has unknown type'); + } + }); + var wbuf = new Buffer('write some bytes'); + fs.write(fd, wbuf, 0, wbuf.length, 0, function(err, written, buffer) { + console.log('wrote ' + written + ' bytes'); + var rbuf = new Buffer(16); + fs.read(fd, rbuf, 0, 16, 0, function(err, num, buf) { + console.log('read from file; error=' + err); + console.log('buffer=' + buf.toString('ascii')); + }); + }); +}); + +fs.open('file.txt', 'w', function(err, fd) { + console.log('opened file error=' + err); +}); + +fs.writeFile('string.txt', 'string data', {}, function(err) { + console.log('wrote string data file error=' + err); +}); +fs.writeFile('buffer.txt', new Buffer('buffer data'), function(err) { + console.log('wrote buffer data file error=' + err); +}); + +setTimeout(function() { + fs.readdir('/', function(err, files) { + for (var i = 0; i < files.length; ++i) { + console.log('files[' + i + ']=' + files[i]); + } + }); +}, 1000); diff --git a/samples/FsSync.js b/samples/FsSync.js new file mode 100644 index 0000000..4d31f39 --- /dev/null +++ b/samples/FsSync.js @@ -0,0 +1,37 @@ +var fs = require('fs'); + +console.log('File System sample'); + +var fd = fs.openSync('testfile.txt', 'w+'); + +var stats = fs.statSync('testfile.txt'); + +if (stats.isFile()) { + console.log('fd is file'); +} else if (stats.isDirectory()) { + console.log('fd is directory'); +} else { + console.log('fd has unknown type'); +} + +var wbuf = new Buffer('write some bytes'); + +var written = fs.writeSync(fd, wbuf, 0, wbuf.length, 0); + +console.log('wrote ' + written + ' bytes'); + + +var rbuf = new Buffer(16); +var ret = fs.readSync(fd, rbuf, 0, 16, 0); +console.log('read ' + ret + ' bytes from file'); +console.log('buffer=' + rbuf.toString('ascii')); + +var fd1 = fs.openSync('file.txt', 'w'); + +fs.writeFileSync('string.txt', 'string data', {}); +fs.writeFileSync('buffer.txt', new Buffer('buffer data')); + +var files = fs.readdirSync('/'); +for (var i = 0; i < files.length; ++i) { + console.log('files[' + i + ']=' + files[i]); +} diff --git a/scripts/analyze.sh b/scripts/analyze.sh index 9beb7aa..e37e69b 100755 --- a/scripts/analyze.sh +++ b/scripts/analyze.sh @@ -236,6 +236,23 @@ if check_for_require aio || check_config_file ZJS_AIO; then echo "export ZJS_AIO=y" >> zjs.conf.tmp fi +if check_for_require fs || check_config_file ZJS_FS; then + >&2 echo Using module: FS + MODULES+=" -DBUILD_MODULE_FS -DBUILD_MODULE_BUFFER" + if [ $BOARD = "arduino_101" ]; then + echo "CONFIG_FS_FAT_FLASH_DISK_W25QXXDV=y" >> prj.conf.tmp + fi + echo "CONFIG_FILE_SYSTEM=y" >> prj.conf.tmp + echo "CONFIG_FILE_SYSTEM_FAT=y" >> prj.conf.tmp + echo "CONFIG_DISK_ACCESS_FLASH=y" >> prj.conf.tmp + + echo "CONFIG_FLASH=y" >> prj.conf.tmp + echo "CONFIG_SPI=y" >> prj.conf.tmp + echo "CONFIG_GPIO=y" >> prj.conf.tmp + echo "export ZJS_FS=y" >> zjs.conf.tmp + echo "export ZJS_BUFFER=y" >> zjs.conf.tmp +fi + if check_for_require i2c || check_config_file ZJS_I2C; then >&2 echo Using module: I2C MODULES+=" -DBUILD_MODULE_I2C" diff --git a/src/Makefile.base b/src/Makefile.base index 9cb3589..e9f32b2 100644 --- a/src/Makefile.base +++ b/src/Makefile.base @@ -45,6 +45,7 @@ obj-$(ZJS_BUFFER) += zjs_buffer.o obj-$(ZJS_CONSOLE) += zjs_console.o obj-$(ZJS_DGRAM) += zjs_dgram.o obj-$(ZJS_EVENTS) += zjs_event.o +obj-$(ZJS_FS) += zjs_fs.o obj-$(ZJS_GPIO) += zjs_gpio.o obj-$(ZJS_BLE) += zjs_ble.o obj-$(ZJS_PWM) += zjs_pwm.o diff --git a/src/zjs_fs.c b/src/zjs_fs.c new file mode 100644 index 0000000..c5ca9b4 --- /dev/null +++ b/src/zjs_fs.c @@ -0,0 +1,1099 @@ +// Copyright (c) 2016, Intel Corporation. +#ifdef BUILD_MODULE_FS + +#include <zephyr.h> +#include <fs.h> + +#include <string.h> + +#include "zjs_callbacks.h" +#include "zjs_common.h" +#include "zjs_util.h" +#include "zjs_buffer.h" + +typedef enum { + // Open for reading, file must already exist + MODE_R, + // Open for reading/writing, file must already exist + MODE_R_PLUS, + // Open file for writing, file will be created or overwritten + MODE_W, + // Open file for reading/writing, file will be created or overwritten + MODE_W_PLUS, + // Open file for appending, file is created if it does not exist + MODE_A, + // Open a file for appending/reading, file is created if it does not exist + MODE_A_PLUS +} FileMode; + +#define MAX_PATH_LENGTH 128 + +/* + * TODO: Someday we may want to keep a list of all open file descriptors. In + * the ashell use case the user may not clean up a descriptor which could + * get leaked. + */ +typedef struct file_handle { + fs_file_t fp; + FileMode mode; + jerry_value_t fd_val; + int error; + uint32_t rpos; +} file_handle_t; + +static jerry_value_t invalid_args(void) +{ + return zjs_error("invalid arguments"); +} + +static file_handle_t* new_file(void) +{ + file_handle_t* handle = zjs_malloc(sizeof(file_handle_t)); + + memset(handle, 0, sizeof(file_handle_t)); + + return handle; +} + +static int file_exists(const char *path) +{ + int res; + struct fs_dirent entry; + + res = fs_stat(path, &entry); + + return !res; +} + +static uint16_t get_mode(char* str) +{ + uint16_t mode = 0; + if (strcmp(str, "r") == 0) { + mode = MODE_R; + } else if (strcmp(str, "r+") == 0) { + mode = MODE_R_PLUS; + } else if (strcmp(str, "w") == 0) { + mode = MODE_W; + } else if (strcmp(str, "w+") == 0) { + mode = MODE_W_PLUS; + } else if (strcmp(str, "a") == 0) { + mode = MODE_A; + } else if (strcmp(str, "a+") == 0) { + mode = MODE_A_PLUS; + } + return mode; +} + +static jerry_value_t is_file(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) +{ + struct fs_dirent* entry; + + if (!jerry_get_object_native_handle(this, (uintptr_t*)&entry)) { + return zjs_error("native handle not found"); + } + if (entry->type == FS_DIR_ENTRY_FILE) { + return jerry_create_boolean(true); + } else { + return jerry_create_boolean(false); + } +} + +static jerry_value_t is_directory(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) +{ + struct fs_dirent* entry; + + if (!jerry_get_object_native_handle(this, (uintptr_t*)&entry)) { + return zjs_error("native handle not found"); + } + if (entry->type == FS_DIR_ENTRY_DIR) { + return jerry_create_boolean(true); + } else { + return jerry_create_boolean(false); + } +} + +static void free_stats(const uintptr_t native) +{ + struct zfs_dirent* entry = (struct zfs_dirent*)native; + if (entry) { + zjs_free(entry); + } +} + +static jerry_value_t create_stats_obj(struct fs_dirent* entry) +{ + + jerry_value_t stats_obj = jerry_create_object(); + + struct fs_dirent* new_entry = zjs_malloc(sizeof(struct fs_dirent)); + if (!new_entry) { + return zjs_error("malloc failed"); + } + memcpy(new_entry, entry, sizeof(struct fs_dirent)); + + jerry_set_object_native_handle(stats_obj, (uintptr_t)new_entry, free_stats); + + zjs_obj_add_function(stats_obj, is_file, "isFile"); + zjs_obj_add_function(stats_obj, is_directory, "isDirectory"); + + return stats_obj; +} + +static jerry_value_t zjs_open(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + if (argc < 3 && async) { + return invalid_args(); + } + if (!jerry_value_is_string(argv[0])) { + return invalid_args(); + } + if (!jerry_value_is_string(argv[1])) { + return invalid_args(); + } + if (async) { + if (!jerry_value_is_function(argv[argc - 1])) { + return invalid_args(); + } + } + + file_handle_t* handle = new_file(); + if (!handle) { + return zjs_error("malloc failed"); + } + handle->fd_val = jerry_create_object(); + + jerry_size_t size = MAX_PATH_LENGTH; + char path[size]; + + zjs_copy_jstring(argv[0], path, &size); + if (!size) { + return zjs_error("size mismatch"); + } + + size = 4; + char mode[size]; + + zjs_copy_jstring(argv[1], mode, &size); + if (!size) { + return zjs_error("size mismatch"); + } + + DBG_PRINT("Opening file: %s, mode: %s\n", path, mode); + + handle->mode = get_mode(mode); + + if ((handle->mode == MODE_R || handle->mode == MODE_R_PLUS) + && !file_exists(path)) { + jerry_release_value(handle->fd_val); + zjs_free(handle); + return zjs_error("file doesn't exist"); + } + + handle->error = fs_open(&handle->fp, path); + if (handle->error != 0) { + ERR_PRINT("could not open file: %s, error=%d\n", path, handle->error); + jerry_release_value(handle->fd_val); + zjs_free(handle); + return zjs_error("could not open file"); + } + + jerry_set_object_native_handle(handle->fd_val, (uintptr_t)handle, NULL); + +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + zjs_callback_id id = zjs_add_callback_once(argv[argc - 1], + this, + handle, + NULL); + + jerry_value_t args[2]; + + args[0] = jerry_create_number(handle->error); + args[1] = handle->fd_val; + + zjs_signal_callback(id, args, sizeof(jerry_value_t) * 2); + + return ZJS_UNDEFINED; + } +#endif + + return handle->fd_val; +} + +static jerry_value_t zjs_open_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_open(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_open_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_open(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_close(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + file_handle_t* handle; + + if (!jerry_value_is_object(argv[0])) { + return invalid_args(); + } + if (async) { + if (!jerry_value_is_function(argv[1])) { + return invalid_args(); + } + } + if (!jerry_get_object_native_handle(argv[0], (uintptr_t*)&handle)) { + return zjs_error("native handle not found"); + } + + handle->error = fs_close(&handle->fp); + +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + zjs_callback_id id = zjs_add_callback_once(argv[1], + this, + handle, + NULL); + + jerry_value_t error = jerry_create_number(handle->error); + + zjs_signal_callback(id, &error, sizeof(jerry_value_t)); + } +#endif + + zjs_free(handle); + + return ZJS_UNDEFINED; +} + +static jerry_value_t zjs_close_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_close(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_close_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_close(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_unlink(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + int ret = 0; + + if (!jerry_value_is_string(argv[0])) { + return invalid_args(); + } + if (async) { + if (!jerry_value_is_function(argv[1])) { + return invalid_args(); + } + } + + jerry_size_t size = MAX_PATH_LENGTH; + char path[size]; + + zjs_copy_jstring(argv[0], path, &size); + if (!size) { + return zjs_error("size mismatch"); + } + + ret = fs_unlink(path); + +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + zjs_callback_id id = zjs_add_callback_once(argv[1], + this, + NULL, + NULL); + + jerry_value_t error = jerry_create_number(ret); + zjs_signal_callback(id, &error, sizeof(jerry_value_t)); + } +#endif + + return ZJS_UNDEFINED; +} + +static jerry_value_t zjs_unlink_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_unlink(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_unlink_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_unlink(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_read(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + file_handle_t* handle; + int err = 0; + + if (argc < 6 && async) { + return invalid_args(); + } + + if (!jerry_value_is_object(argv[0])) { + return invalid_args(); + } + if (!jerry_value_is_object(argv[1])) { + return invalid_args(); + } + if (!jerry_value_is_number(argv[2])) { + return invalid_args(); + } + if (!jerry_value_is_number(argv[3])) { + return invalid_args(); + } + if (!jerry_value_is_number(argv[4]) && !jerry_value_is_null(argv[4])) { + return invalid_args(); + } + if (async) { + if (!jerry_value_is_function(argv[5])) { + return invalid_args(); + } + } + if (!jerry_get_object_native_handle(argv[0], (uintptr_t*)&handle)) { + return zjs_error("native handle not found"); + } + + if (handle->mode == MODE_W || handle->mode == MODE_A) { + return zjs_error("file is not open for reading"); + } + + zjs_buffer_t* buffer = zjs_buffer_find(argv[1]); + double offset = jerry_get_number_value(argv[2]); + double length = jerry_get_number_value(argv[3]); + + if (offset < 0 || length < 0) { + return invalid_args(); + } + if (offset >= buffer->bufsize) { + return zjs_error("offset overflows buffer"); + } + if (offset + length > buffer->bufsize) { + return zjs_error("offset + length overflows buffer"); + } + + // if mode == a+ + if (handle->mode == MODE_A_PLUS) { + // mode is a+, seek to read position + if (fs_seek(&handle->fp, handle->rpos, SEEK_SET) != 0) { + return zjs_error("error seeking to position"); + } + } + if (jerry_value_is_number(argv[4])) { + // if position was a number, set as the new read position + double position = jerry_get_number_value(argv[4]); + if (position < 0) { + return invalid_args(); + } + handle->rpos = position; + // if a position was specified, seek to it before reading + if (fs_seek(&handle->fp, (uint32_t)position, SEEK_SET) != 0) { + return zjs_error("error seeking to position"); + } + } + + DBG_PRINT("reading into fp=%p, buffer=%p, offset=%lu, length=%lu\n", &handle->fp, buffer->buffer, (uint32_t)offset, (uint32_t)length); + + uint32_t ret = fs_read(&handle->fp, buffer->buffer + (uint32_t)offset, (uint32_t)length); + + if (ret != (uint32_t)length) { + DBG_PRINT("could not read %lu bytes, only %lu were read\n", (uint32_t)length, ret); + err = -1; + } + handle->rpos += ret; + +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + jerry_value_t args[3]; + + args[0] = jerry_create_number(err); + args[1] = jerry_create_number(ret); + args[2] = argv[1]; + + zjs_callback_id id = zjs_add_callback_once(argv[5], + this, + NULL, + NULL); + + zjs_signal_callback(id, args, sizeof(jerry_value_t) * 3); + + return ZJS_UNDEFINED; + } +#endif + return jerry_create_number(ret); +} + +static jerry_value_t zjs_read_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_read(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_read_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_read(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_write(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + file_handle_t* handle; + double position = 0; + double offset = 0; + double length = 0; + uint8_t from_cur = 0; +#ifdef ZJS_FS_ASYNC_APIS + jerry_value_t js_cb = ZJS_UNDEFINED; +#endif + + if (argc < 2) { + return invalid_args(); + } + if (jerry_value_is_object(argv[1])) { + // buffer + if (argc > 2) { + if (jerry_value_is_number(argv[2])) { + offset = jerry_get_number_value(argv[2]); + } else { + return invalid_args(); + } + } + if (argc > 3) { + if (jerry_value_is_number(argv[3])) { + length = jerry_get_number_value(argv[3]); + } else { + return invalid_args(); + } + } + if (argc > 4) { + if (jerry_value_is_number(argv[4])) { + position = jerry_get_number_value(argv[4]); + } else { + // position != number + from_cur = 1; + } + } +#ifdef ZJS_FS_ASYNC_APIS + if (!jerry_value_is_function(argv[argc - 1])) { + return invalid_args(); + } else { + js_cb = argv[argc - 1]; + } +#endif + } else if (jerry_value_is_string(argv[1])) { + /* + * TODO: Eventually support string if its desired. It makes argument + * parsing a huge pain, so just supporting buffer for now seems + * like the right decision. + */ + return zjs_error("string input not supported"); + } else { + return invalid_args(); + } + + if (!jerry_get_object_native_handle(argv[0], (uintptr_t*)&handle)) { + return zjs_error("native handle not found"); + } + + if (handle->mode == MODE_R) { + return zjs_error("file is not open for writing"); + } + + zjs_buffer_t* buffer = zjs_buffer_find(argv[1]); + + if (offset && !length) { + length = buffer->bufsize - offset; + } else if (!length) { + length = buffer->bufsize; + } + + if (offset < 0 || length < 0 || position < 0) { + return invalid_args(); + } + if (offset >= buffer->bufsize) { + return zjs_error("offset overflows buffer"); + } + if (offset + length > buffer->bufsize) { + return zjs_error("offset + length overflows buffer"); + } + + if (handle->mode == MODE_A || handle->mode == MODE_A_PLUS) { + // if in append mode, seek to end (ignoring position parameter) + if (fs_seek(&handle->fp, 0, SEEK_END) != 0) { + return zjs_error("error seeking start"); + } + } else if (!from_cur) { + // if a position was specified, seek to it before writing + if (fs_seek(&handle->fp, (uint32_t)position, SEEK_SET) != 0) { + return zjs_error("error seeking to position\n"); + } + } + + DBG_PRINT("writing to fp=%p, buffer=%p, offset=%lu, length=%lu\n", &handle->fp, buffer->buffer, (uint32_t)offset, (uint32_t)length); + + uint32_t written = fs_write(&handle->fp, buffer->buffer + (uint32_t)offset, (uint32_t)length); + + if (written != (uint32_t)length) { + DBG_PRINT("could not write %lu bytes, only %lu were written\n", (uint32_t)length, written); + } + +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + jerry_value_t args[3]; + + args[0] = jerry_create_number(0); + args[1] = jerry_create_number(written); + args[2] = argv[1]; + + zjs_callback_id id = zjs_add_callback_once(js_cb, + this, + NULL, + NULL); + + zjs_signal_callback(id, args, sizeof(jerry_value_t) * 3); + + return ZJS_UNDEFINED; + } +#endif + return jerry_create_number(written); +} + +static jerry_value_t zjs_write_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_write(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_write_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_write(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_truncate(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + fs_file_t fp; + if (!jerry_value_is_number(argv[1])) { + return invalid_args(); + } + if (async) { + if (!jerry_value_is_function(argv[2])) { + return invalid_args(); + } + } + if (jerry_value_is_object(argv[0])) { + file_handle_t* handle; + if (!jerry_get_object_native_handle(argv[0], (uintptr_t*)&handle)) { + return zjs_error("native handle not found"); + } + fp = handle->fp; + } else if (jerry_value_is_string(argv[0])) { + jerry_size_t size = MAX_PATH_LENGTH; + char path[size]; + + zjs_copy_jstring(argv[0], path, &size); + if (!size) { + return zjs_error("size mismatch"); + } + if (!fs_open(&fp, path)) { + return zjs_error("error opening file for truncation"); + } + } else { + return invalid_args(); + } + + uint32_t length = jerry_get_number_value(argv[1]); + + if (!fs_truncate(&fp, length)) { + return zjs_error("error calling fs_truncate()"); + } + +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + zjs_callback_id id = zjs_add_callback_once(argv[2], + this, + NULL, + NULL); + + zjs_signal_callback(id, NULL, 0); + } +#endif + return ZJS_UNDEFINED; +} + +static jerry_value_t zjs_truncate_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_truncate(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_truncate_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_truncate(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_mkdir(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + jerry_value_t js_cb = ZJS_UNDEFINED; + uint32_t mode = MODE_A_PLUS; + jerry_size_t size; + if (!jerry_value_is_string(argv[0])) { + return invalid_args(); + } + if (argc > 2) { + if (async) { + if (!jerry_value_is_function(argv[2])) { + return invalid_args(); + } else { + js_cb = argv[2]; + } + } + if (!jerry_value_is_string(argv[1])) { + return invalid_args(); + } else { + size = 4; + char mode_str[size]; + + zjs_copy_jstring(argv[1], mode_str, &size); + if (!size) { + return zjs_error("size mismatch"); + } + + mode = get_mode(mode_str); + } + } else { + if (async) { + if (!jerry_value_is_function(argv[1])) { + return invalid_args(); + } else { + js_cb = argv[1]; + } + } + } + size = MAX_PATH_LENGTH; + char path[size]; + + zjs_copy_jstring(argv[0], path, &size); + if (!size) { + return zjs_error("size mismatch"); + } + + if (!fs_mkdir(path)) { + return zjs_error("error creating directory"); + } +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + zjs_callback_id id = zjs_add_callback_once(js_cb, + this, + NULL, + NULL); + + zjs_signal_callback(id, NULL, 0); + } +#endif + + return ZJS_UNDEFINED; +} + +static jerry_value_t zjs_mkdir_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_mkdir(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_mkdir_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_mkdir(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_readdir(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + jerry_value_t array; + + if (!jerry_value_is_string(argv[0])) { + return zjs_error("first parameter was not path"); + } + jerry_size_t size = MAX_PATH_LENGTH; + char path[size]; + + zjs_copy_jstring(argv[0], path, &size); + if (!size) { + return zjs_error("size mismatch"); + } + + int num_files = 0; + int res; + fs_dir_t dp; + static struct fs_dirent entry; + + res = fs_opendir(&dp, path); + if (res) { + return zjs_error("Error opening dir"); + } + + DBG_PRINT("Searching for files and sub directories in %s\n", path); + for (;;) { + res = fs_readdir(&dp, &entry); + + /* entry.name[0] == 0 means end-of-dir */ + if (res || entry.name[0] == 0) { + break; + } + + /* Delete file or sub directory */ + num_files++; + + DBG_PRINT("found file %s\n", entry.name); + } + + fs_closedir(&dp); + + res = fs_opendir(&dp, path); + if (res) { + return zjs_error("Error opening dir"); + } + + DBG_PRINT("Adding files and sub directories in %s to array\n", path); + + array = jerry_create_array(num_files); + + uint32_t i; + for (i = 0; i < num_files; ++i) { + res = fs_readdir(&dp, &entry); + + /* entry.name[0] == 0 means end-of-dir */ + if (res || entry.name[0] == 0) { + break; + } + + jerry_value_t value = jerry_create_string(entry.name); + + jerry_set_property_by_index (array, i, value); + } + + fs_closedir(&dp); + +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + zjs_callback_id id = zjs_add_callback_once(argv[1], + this, + NULL, + NULL); + + jerry_value_t args[2]; + + args[0] = jerry_create_number(res); + args[1] = array; + + zjs_signal_callback(id, args, sizeof(jerry_value_t) * 2); + + return ZJS_UNDEFINED; + } +#endif + return array; +} + +static jerry_value_t zjs_readdir_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_readdir(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_readdir_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_readdir(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_stat(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + if (!jerry_value_is_string(argv[0])) { + return invalid_args(); + } + if (async) { + if (!jerry_value_is_function(argv[1])) { + return invalid_args(); + } + } + jerry_size_t size = MAX_PATH_LENGTH; + char path[size]; + + zjs_copy_jstring(argv[0], path, &size); + if (!size) { + return zjs_error("size mismatch"); + } + + int ret; + struct fs_dirent entry; + + ret = fs_stat(path, &entry); + +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + jerry_value_t args[2]; + + args[0] = jerry_create_number(ret); + args[1] = create_stats_obj(&entry); + + zjs_callback_id id = zjs_add_callback_once(argv[1], + this, + NULL, + NULL); + + zjs_signal_callback(id, args, sizeof(jerry_value_t) * 2); + + return ZJS_UNDEFINED; + } +#endif + return create_stats_obj(&entry); +} + +static jerry_value_t zjs_stat_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_stat(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_stat_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_stat(function_obj, this, argv, argc, 1); +} +#endif + +static jerry_value_t zjs_write_file(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc, + uint8_t async) +{ + uint8_t is_buf = 0; + jerry_size_t size; + int error = 0; + jerry_value_t js_cb = ZJS_UNDEFINED; + char* data = NULL; + uint32_t length; + if (!jerry_value_is_string(argv[0])) { + return invalid_args(); + } + if (jerry_value_is_string(argv[1])) { + size = 256; + data = zjs_alloc_from_jstring(argv[1], &size); + length = size; + } else if (jerry_value_is_object(argv[1])) { + zjs_buffer_t* buffer = zjs_buffer_find(argv[1]); + data = buffer->buffer; + length = buffer->bufsize; + is_buf = 1; + } else { + return invalid_args(); + } + // Options provided + if (argc > 3) { + if (!jerry_value_is_object(argv[2])) { + if (data && !is_buf) { + zjs_free(data); + } + return invalid_args(); + } + // Options object has no effect on Zephyr, 'mode' and 'flag' properties + // are not used for opening a file, so their values are irrelevant +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + if (!jerry_value_is_function(argv[3])) { + return invalid_args(); + } else { + js_cb = argv[3]; + } + } +#endif + } else if (async) { + js_cb = argv[argc - 1]; + } + + size = 32; + char* path = zjs_alloc_from_jstring(argv[0], &size); + if (!path) { + if (data && !is_buf) { + zjs_free(data); + } + return zjs_error("path string too long\n"); + } + + fs_file_t fp; + error = fs_open(&fp, path); + if (error != 0) { + ERR_PRINT("error opening file, error=%d\n", error); + goto Finished; + } + ssize_t written = fs_write(&fp, data, length); + + if (written != length) { + ERR_PRINT("could not write %lu bytes, only %lu were written\n", length, written); + error = -1; + } + + error = fs_close(&fp); + if (error != 0) { + ERR_PRINT("error closing file\n"); + } + +Finished: +#ifdef ZJS_FS_ASYNC_APIS + if (async) { + zjs_callback_id id = zjs_add_callback_once(js_cb, + this, + NULL, + NULL); + + jerry_value_t err = jerry_create_number(error); + + zjs_signal_callback(id, &err, sizeof(jerry_value_t)); + } +#endif + if (data && !is_buf) { + zjs_free(data); + } + if (path) { + zjs_free(path); + } + return ZJS_UNDEFINED; +} + +static jerry_value_t zjs_write_file_sync(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_write_file(function_obj, this, argv, argc, 0); +} + +#ifdef ZJS_FS_ASYNC_APIS +static jerry_value_t zjs_write_file_async(const jerry_value_t function_obj, + const jerry_value_t this, + const jerry_value_t argv[], + const jerry_length_t argc) { + return zjs_write_file(function_obj, this, argv, argc, 1); +} +#endif + +jerry_value_t zjs_fs_init() +{ + jerry_value_t fs = jerry_create_object(); + + zjs_obj_add_function(fs, zjs_open_sync, "openSync"); + zjs_obj_add_function(fs, zjs_close_sync, "closeSync"); + zjs_obj_add_function(fs, zjs_unlink_sync, "unlinkSync"); + zjs_obj_add_function(fs, zjs_unlink_sync, "rmdirSync"); + zjs_obj_add_function(fs, zjs_read_sync, "readSync"); + zjs_obj_add_function(fs, zjs_write_sync, "writeSync"); + zjs_obj_add_function(fs, zjs_truncate_sync, "truncateSync"); + zjs_obj_add_function(fs, zjs_mkdir_sync, "mkdirSync"); + zjs_obj_add_function(fs, zjs_readdir_sync, "readdirSync"); + zjs_obj_add_function(fs, zjs_stat_sync, "statSync"); + zjs_obj_add_function(fs, zjs_write_file_sync, "writeFileSync"); + +#ifdef ZJS_FS_ASYNC_APIS + zjs_obj_add_function(fs, zjs_open_async, "open"); + zjs_obj_add_function(fs, zjs_close_async, "close"); + zjs_obj_add_function(fs, zjs_unlink_async, "unlink"); + zjs_obj_add_function(fs, zjs_unlink_async, "rmdir"); + zjs_obj_add_function(fs, zjs_read_async, "read"); + zjs_obj_add_function(fs, zjs_write_async, "write"); + zjs_obj_add_function(fs, zjs_truncate_async, "truncate"); + zjs_obj_add_function(fs, zjs_mkdir_async, "mkdir"); + zjs_obj_add_function(fs, zjs_readdir_async, "readdir"); + zjs_obj_add_function(fs, zjs_stat_async, "stat"); + zjs_obj_add_function(fs, zjs_write_file_async, "writeFile"); +#endif + + return fs; +} +#endif diff --git a/src/zjs_fs.h b/src/zjs_fs.h new file mode 100644 index 0000000..3c90517 --- /dev/null +++ b/src/zjs_fs.h @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Intel Corporation. + +#ifndef SRC_ZJS_FS_H_ +#define SRC_ZJS_FS_H_ + +jerry_value_t zjs_fs_init(); + +#endif /* SRC_ZJS_FS_H_ */ diff --git a/src/zjs_modules.c b/src/zjs_modules.c index 531b595..7058a66 100644 --- a/src/zjs_modules.c +++ b/src/zjs_modules.c @@ -39,6 +39,7 @@ #include "zjs_i2c.h" #include "zjs_pwm.h" #include "zjs_uart.h" +#include "zjs_fs.h" #ifdef CONFIG_BOARD_ARDUINO_101 #include "zjs_a101_pins.h" #endif @@ -83,6 +84,9 @@ module_t zjs_modules_array[] = { #ifdef BUILD_MODULE_I2C { "i2c", zjs_i2c_init }, #endif +#ifdef BUILD_MODULE_FS + { "fs", zjs_fs_init }, +#endif #ifdef CONFIG_BOARD_ARDUINO_101 #ifdef BUILD_MODULE_A101 { "arduino101_pins", zjs_a101_init }, diff --git a/tests/test-fs.js b/tests/test-fs.js new file mode 100644 index 0000000..5a797fa --- /dev/null +++ b/tests/test-fs.js @@ -0,0 +1,195 @@ +var fs = require('fs'); + +var total = 0; +var passed = 0; +var pass = true; + +function assert(actual, description) { + total += 1; + + var label = "\033[1m\033[31mFAIL\033[0m"; + if (actual === true) { + passed += 1; + label = "\033[1m\033[32mPASS\033[0m"; + } + + console.log(label + " - " + description); +} + +function expectThrow(description, func) { + var threw = false; + try { + func(); + } + catch (err) { + threw = true; + } + assert(threw, description); +} + +// Clean up any test files left from previous tests +var stats = fs.statSync('testfile.txt'); +if (stats.isFile() || stats.isDirectory()) { + fs.unlinkSync('testfile.txt'); + console.log('removing testfile.txt'); +} + +// test file creation +var fd = fs.openSync('testfile.txt', 'a'); +fs.closeSync(fd); +var fd_stats = fs.statSync('testfile.txt'); +var success = false; +if (fd_stats.isFile()) { + success = true; +} +assert(success, "create file"); + +// test file removal +fs.unlinkSync('testfile.txt'); +fd_stats = fs.statSync('testfile.txt'); +success = false; +if (!fd_stats.isFile()) { + success = true; +} +assert(success, "remove file"); + +//test invalid file open (on a non-existing file) +expectThrow("open(r) on non-existing file", function () { + fd = fs.openSync('testfile.txt', 'r'); +}); + +expectThrow("open(r+) on non-existing file", function () { + fd = fs.openSync('testfile.txt', 'r+'); +}); + +// Test appending options +fs.writeFileSync("testfile.txt", new Buffer("test")); + +fd = fs.openSync('testfile.txt', 'a'); + +// test reading from 'a' file +expectThrow("can't read from append file", function() { + var rbuf = new Buffer(wbuf.length); + var rlen = fs.readSync(fd, rbuf, 0, rbuf.length, 0); +}); + +// test writing to end of 'a' file +var wbuf = new Buffer('write'); +var wlen = fs.writeSync(fd, wbuf, 0, wbuf.length, 0); +assert((wbuf.length == wlen), "writing to append file") + +fs.closeSync(fd); + +// test reading from a+ file +fd = fs.openSync('testfile.txt', 'a+'); +var rbuf = new Buffer(4); +var rlen = fs.readSync(fd, rbuf, 0, 4, 0); +assert((rbuf.length == rlen), "read from a+ file") +assert((rbuf.toString('ascii') == "test"), + "read correct data from a+ file: " + rbuf.toString('ascii')); + +// test that read position was kept +rbuf = new Buffer(5); +rlen = fs.readSync(fd, rbuf, 0, 5, null); +assert((rbuf.length == rlen), "read from a+ file (again)") +assert((rbuf.toString('ascii') == "write"), + "read correct data from a+ file: " + rbuf.toString('ascii')); + +fs.closeSync(fd); + +// test reading past end of file +fd = fs.openSync('testfile.txt', 'r'); +rbuf = new Buffer(12); +rlen = fs.readSync(fd, rbuf, 0, 12, 0); +assert((rlen < rbuf.length), "try reading past end of file"); +assert((rbuf.toString('ascii') == "testwrite"), + "read data that existed in r file: " + rbuf.toString('ascii')); + +// test write to read only file +expectThrow("tried writing to read only file", function() { + fs.writeSync(fd, new Buffer("dummy"), 0, 5, 0); +}); + +fs.closeSync(fd); + +// test null position parameter +fd = fs.openSync('testfile.txt', 'r'); +rbuf = new Buffer(4); +rlen = fs.readSync(fd, rbuf, 0, 4, null); +assert((rlen == rbuf.length), + "try reading with null position first: " + rbuf.length); +assert((rbuf.toString('ascii') == "test"), + "read data correct with null position first: " + rbuf.toString('ascii')); + +rbuf = new Buffer(5); +rlen = fs.readSync(fd, rbuf, 0, 5, null); +assert((rlen == rbuf.length), + "try reading with null position (again): " + rbuf.length); +assert((rbuf.toString('ascii') == "write"), + "read data correct with null position (again): " + rbuf.toString('ascii')); + +rbuf = new Buffer(4); +rlen = fs.readSync(fd, rbuf, 0, 4, 0); +assert((rlen == rbuf.length), + "try reading with 0 position (after null): " + rbuf.length); +assert((rbuf.toString('ascii') == "test"), + "read data correct with 0 position (after null): " + rbuf.toString('ascii')); + +fs.closeSync(fd); + +// test write with only buffer +fd = fs.openSync('testfile.txt', 'w+'); +wbuf = new Buffer("writetest"); +wlen = fs.writeSync(fd, wbuf); +assert((wlen == wbuf.length), "write with just buffer: " + wlen); + +rbuf = new Buffer(9); +rlen = fs.readSync(fd, rbuf, 0, rbuf.length, 0); + +assert((rlen == rbuf.length), + "try reading (write with no length): " + rbuf.toString('ascii')); +assert((rbuf.toString('ascii') == "writetest"), + "read data correct (write with no length): " + rbuf.toString('ascii')); + +// test write with buffer + offset +wbuf = new Buffer("_____12345"); +wlen = fs.writeSync(fd, wbuf, 5); +assert((wlen == 5), "write with just buffer: " + wlen); + +rbuf = new Buffer(5); +rlen = fs.readSync(fd, rbuf, 0, rbuf.length, 0); +assert((rlen == rbuf.length), + "try reading (write with buf/off): " + rbuf.toString('ascii')); +assert((rbuf.toString('ascii') == "12345"), + "read data correct (write with buf/off): " + rbuf.toString('ascii')); + +// test write with buffer + offset + length +wbuf = new Buffer("_____123456789"); +wlen = fs.writeSync(fd, wbuf, 5, 9); +assert((wlen == 9), "write with just buffer: " + wlen); + +rbuf = new Buffer(9); +rlen = fs.readSync(fd, rbuf, 0, rbuf.length, 0); +assert((rlen == rbuf.length), + "try reading (write with buf/off/len): " + rbuf.toString('ascii')); +assert((rbuf.toString('ascii') == "123456789"), + "read data correct (write with buf/off/len): " + rbuf.toString('ascii')); + +// test write with buffer + offset + length + position +wbuf = new Buffer("_____test"); +wlen = fs.writeSync(fd, wbuf, 5, 4, 5); +assert((wlen == 4), "write with just buffer: " + wlen); + +rbuf = new Buffer(9); +rlen = fs.readSync(fd, rbuf, 0, rbuf.length, 0); +assert((rlen == rbuf.length), + "try reading (write with buf/off/len/pos): " + rbuf.toString('ascii')); +assert((rbuf.toString('ascii') == "12345test"), + "read data correct (write with buf/off/len/pos): " + rbuf.toString('ascii')); + + +fs.closeSync(fd); + +fs.unlinkSync('testfile.txt'); + +console.log("TOTAL: " + passed + " of " + total + " passed"); |