├── .gitignore ├── example.js ├── cli.js ├── test.js ├── binding.gyp ├── package.json ├── appveyor.yml ├── README.md ├── LICENSE ├── .travis.yml ├── index.js └── index.c /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const fsx = require('./') 2 | 3 | fsx.set(__filename, 'user.hello', Buffer.from('worlds'), function () { 4 | fsx.get(__filename, 'user.hello', console.log) 5 | }) 6 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fsx = require('./') 4 | const args = process.argv.slice(2) 5 | 6 | if (args[0] === 'set') { 7 | fsx.set(args[1], args[2], args[3], function (err) { 8 | if (err) throw err 9 | }) 10 | } else if (args[0] === 'get') { 11 | fsx.get(args[1], args[2], function (err, buf) { 12 | if (err) throw err 13 | if (!buf) console.log('(no attribute found)') 14 | else console.log(buf.toString()) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | const fsx = require('./') 3 | 4 | tape('basic', function (t) { 5 | const random = Buffer.from(Math.random().toString(16).slice(2)) 6 | fsx.set(__filename, 'user.test', random, function (err) { 7 | t.error(err, 'no error') 8 | fsx.get(__filename, 'user.test', function (err, buf) { 9 | t.error(err, 'no error') 10 | t.same(buf, random, 'correct attribute') 11 | t.end() 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [{ 3 | "target_name": "fs_extended_attributes", 4 | "include_dirs": [ 5 | " 16384) throw new Error('Data must be <= 16KB') 25 | data.copy(this.handle, 1 + 2 * 2048) 26 | this.dataLength = data.length 27 | } 28 | 29 | Worker.prototype.getData = function () { 30 | const data = this.handle.slice(1 + 2 * 2048, 1 + 2 * 2048 + this.dataLength) 31 | const cpy = Buffer.allocUnsafe(data.length) 32 | data.copy(cpy) 33 | return cpy 34 | } 35 | 36 | Worker.prototype.run = function (type, path, key, data, cb) { 37 | this.stringType = type === 0 ? ASYNC_GET : ASYNC_SET 38 | this.handle[0] = type 39 | this._write(1, path) 40 | this._write(1 + 2048, key) 41 | if (data) this.setData(data) 42 | else this.dataLength = 0 43 | if (cb) this.callback = cb 44 | binding.fsx_run(this.handle, this.dataLength, this.stringType) 45 | } 46 | 47 | Worker.prototype._write = function (offset, str) { 48 | offset += this.handle.write(str, offset, 2047) 49 | this.handle[offset] = 0 50 | } 51 | 52 | Worker.prototype._ondone = function (hadError, len) { 53 | const get = this.handle[0] === 0 54 | const err = hadError 55 | ? new Error((get ? 'get' : 'set') + ' fs attribute failed') 56 | : null 57 | const cb = this.callback 58 | this.callback = null 59 | this.dataLength = len 60 | free.push(this) 61 | if (!cb) return 62 | if (err) return cb(err, null) 63 | const data = (get && len) ? this.getData() : null 64 | cb(null, data) 65 | } 66 | 67 | function alloc () { 68 | if (free.length) return free.pop() 69 | const worker = new Worker() 70 | workers.push(worker) 71 | return worker 72 | } 73 | 74 | function set (path, name, val, cb) { 75 | if (!Buffer.isBuffer(val)) val = Buffer.from(val) 76 | if (IS_WINDOWS) fs.writeFile(path + ':' + name, val, cb || noop) 77 | else alloc().run(1, path, name, val, cb) 78 | } 79 | 80 | function get (path, name, cb) { 81 | if (IS_WINDOWS) fs.readFile(path + ':' + name, onget(cb)) 82 | else alloc().run(0, path, name, null, cb) 83 | } 84 | 85 | function noop () {} 86 | 87 | function onget (cb) { 88 | return function (err, buf) { 89 | if (err && err.code === 'ENOENT') cb(null, null) 90 | else cb(err, buf) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /index.c: -------------------------------------------------------------------------------- 1 | #ifndef _WIN32 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #define FSX_MAX_PATH 2048 11 | #define FSX_MAX_VALUE 16384 12 | 13 | typedef struct { 14 | uint8_t type; 15 | const char path[FSX_MAX_PATH]; 16 | const char key[FSX_MAX_PATH]; 17 | char value[FSX_MAX_VALUE]; 18 | uint32_t value_length; 19 | napi_async_work worker; 20 | napi_ref callback; 21 | napi_ref ctx; 22 | int error; 23 | } fsx_t; 24 | 25 | static void fsx_execute_callback (napi_env env, void* data) { 26 | fsx_t *self = (fsx_t *) data; 27 | 28 | ssize_t ret = -1; 29 | size_t val_len = self->value_length; 30 | const char *path = (const char *) &(self->path); 31 | const char *key = (const char *) &(self->key); 32 | char *val = (char *) self->value; 33 | 34 | if (self->type == 0) { 35 | #ifdef __APPLE__ 36 | ret = getxattr(path, key, val, FSX_MAX_VALUE, 0, 0); 37 | #else 38 | ret = getxattr(path, key, val, FSX_MAX_VALUE); 39 | #endif 40 | if (ret < 0 && errno == ENODATA) ret = 0; 41 | } 42 | 43 | if (self->type == 1) { 44 | #ifdef __APPLE__ 45 | ret = setxattr(path, key, val, val_len, 0, 0); 46 | #else 47 | ret = setxattr(path, key, val, val_len, 0); 48 | #endif 49 | } 50 | 51 | self->error = ret < 0 ? 1 : 0; 52 | if (ret >= 0) self->value_length = (uint32_t) ret; 53 | } 54 | 55 | static void fsx_complete_callback (napi_env env, napi_status status, void* data) { 56 | fsx_t *self = (fsx_t *) data; 57 | 58 | napi_delete_async_work(env, self->worker); 59 | 60 | napi_handle_scope scope; 61 | napi_open_handle_scope(env, &scope); 62 | napi_value ctx; 63 | napi_get_reference_value(env, self->ctx, &ctx); 64 | napi_value callback; 65 | napi_get_reference_value(env, self->callback, &callback); 66 | napi_value argv[2]; 67 | napi_create_uint32(env, self->error, &(argv[0])); 68 | napi_create_uint32(env, self->value_length, &(argv[1])); 69 | napi_make_callback(env, NULL, ctx, callback, 2, argv, NULL); 70 | napi_close_handle_scope(env, scope); 71 | } 72 | 73 | NAPI_METHOD(fsx_init) { 74 | NAPI_ARGV(3) 75 | NAPI_ARGV_BUFFER_CAST(fsx_t *, data, 0) 76 | 77 | napi_create_reference(env, argv[1], 1, &(data->ctx)); 78 | napi_create_reference(env, argv[2], 1, &(data->callback)); 79 | 80 | return NULL; 81 | } 82 | 83 | NAPI_METHOD(fsx_run) { 84 | NAPI_ARGV(3) 85 | NAPI_ARGV_BUFFER_CAST(fsx_t *, data, 0) 86 | NAPI_ARGV_UINT32(value_len, 1) 87 | 88 | data->value_length = value_len; 89 | 90 | napi_create_async_work( 91 | env, 92 | NULL, // resource 93 | argv[2], 94 | fsx_execute_callback, 95 | fsx_complete_callback, 96 | data, 97 | &(data->worker) 98 | ); 99 | 100 | napi_queue_async_work(env, data->worker); 101 | 102 | return NULL; 103 | } 104 | 105 | NAPI_INIT() { 106 | NAPI_EXPORT_FUNCTION(fsx_init) 107 | NAPI_EXPORT_FUNCTION(fsx_run) 108 | NAPI_EXPORT_SIZEOF(fsx_t) 109 | } 110 | 111 | #endif 112 | --------------------------------------------------------------------------------