From 8e27567857371286ce7b96d433cb320a8f758064 Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Fri, 10 Apr 2020 20:34:00 +0200 Subject: [PATCH] imported code from webfuse-example --- dist/webfuse.js | 1 + dist/webufse.js | 1 - package.json | 13 ++- src/bad_state.js | 15 +++ src/client.js | 223 ++++++++++++++++++++++++++++++++++++++++++ src/file_mode.js | 10 ++ src/index.js | 8 +- src/provider.js | 30 ++++++ test/mocha.opts | 1 + test/test_provider.js | 20 ++++ webpack.config.js | 4 +- 11 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 dist/webfuse.js delete mode 100644 dist/webufse.js create mode 100644 src/bad_state.js create mode 100644 src/client.js create mode 100644 src/file_mode.js create mode 100644 src/provider.js create mode 100644 test/mocha.opts create mode 100644 test/test_provider.js diff --git a/dist/webfuse.js b/dist/webfuse.js new file mode 100644 index 0000000..4518e4f --- /dev/null +++ b/dist/webfuse.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.webfuse=t():e.webfuse=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var s=t[n]={i:n,l:!1,exports:{}};return e[n].call(s.exports,s,s.exports,r),s.l=!0,s.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)r.d(n,s,function(t){return e[t]}.bind(null,s));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";r.r(t),r.d(t,"BadState",(function(){return n})),r.d(t,"Provider",(function(){return s})),r.d(t,"FileMode",(function(){return i})),r.d(t,"Client",(function(){return o}));class n extends Error{static get BAD(){return 1}static get NOT_IMPLEMENTED(){return 2}static get TIMEOUT(){return 3}static get FORMAT(){return 4}static get NO_ENTRY(){return 101}static get NO_ACCESS(){return 102}constructor(e){super("Bad State"),this.code=e}}class s{async lookup(e,t){throw new n(n.NOT_IMPLEMENTED)}async getattr(e){throw new n(n.NOT_IMPLEMENTED)}async readdir(e){throw new n(n.NOT_IMPLEMENTED)}async open(e,t){throw new n(n.NOT_IMPLEMENTED)}close(e,t,r){}async read(e,t,r,s){throw new n(n.NOT_IMPLEMENTED)}}class i{static get ACCESS_MODE(){return 3}static get READONLY(){return 0}static get WRITEONLY(){return 1}static get READWRITE(){return 2}static get CREATE(){return 64}static get EXCLUSIVE(){return 128}static get TRUNKATE(){return 512}static get APPEND(){return 1024}}class o{static get _PROTOCOL(){return"webfuse-adapter-server"}constructor(e){this._provider={},this._pendingRequests={},this._id=0,this._ws=null,this.onopen=()=>{},this.onclose=()=>{},this.onerror=()=>{}}connectTo(e){this.disconnect(),this._ws=new WebSocket(e,o._PROTOCOL),this._ws.onopen=this.onopen,this._ws.onclose=this.onclose,this._ws.onerror=this.onerror,this._ws.onmessage=e=>{this._onmessage(e)}}_invokeRequest(e,t){this._id+=1;const r=this._id,n={method:e,params:t,id:r};return new Promise((e,t)=>{this._pendingRequests[r]={resolve:e,reject:t},this._ws.send(JSON.stringify(n))})}authenticate(e,t){return this._invokeRequest("authenticate",[e,t])}addProvider(e,t){this._provider[e]=t;const r={method:"add_filesystem",params:[e],id:23};this._ws.send(JSON.stringify(r))}disconnect(){this._ws&&(this._ws.close(),this._ws=null)}isConnected(){return this._ws&&this._ws.readyState===WebSocket.OPEN}_isRequest(e){return"string"==typeof e.method&&"params"in e}_isResponse(e){return"number"==typeof e.id&&("result"in e||"error"in e)}_removePendingRequest(e){let t=null;return e in this._pendingRequests&&(t=this._pendingRequests[e],Reflect.deleteProperty(this._pendingRequests,e)),t}_onmessage(e){try{const t=JSON.parse(e.data);if(this._isRequest(t)){const e=t.method,r=t.id,n=t.params;"number"==typeof r?this._invoke(e,n,r):this._notify(e,n)}else if(this._isResponse(t)){const e=t.id,r=t.result,n=t.error,s=this._removePendingRequest(e);s&&(r?s.resolve(r):s.reject(n))}}catch(e){}}_invoke(e,t,r){this._invokeAsync(e,t).then(e=>{const t={result:e,id:r};this._ws.send(JSON.stringify(t))}).catch(e=>{const t={error:{code:e.code||n.BAD},id:r};this._ws.send(JSON.stringify(t))})}async _invokeAsync(e,t){switch(e){case"lookup":return this._lookup(t);case"getattr":return this._getattr(t);case"readdir":return this._readdir(t);case"open":return this._open(t);case"read":return this._read(t);default:throw new n(n.NOT_IMPLEMENTED)}}_notify(e,t){switch(e){case"close":this._close(t);break;default:throw new Error(`Invalid method: "${e}"`)}}_getProvider(e){if(e in this._provider)return this._provider[e];throw new Error("Unknown provider")}async _lookup([e,t,r]){return this._getProvider(e).lookup(t,r)}async _getattr([e,t]){return this._getProvider(e).getattr(t)}async _readdir([e,t]){return this._getProvider(e).readdir(t)}async _open([e,t,r]){return this._getProvider(e).open(t,r)}_close([e,t,r,n]){this._getProvider(e).close(t,r,n)}async _read([e,t,r,s,i]){const o=this._getProvider(e),u=await o.read(t,r,s,i);if("string"==typeof u)return{data:btoa(u),format:"base64",count:u.length};throw new n(n.BAD)}}}])})); \ No newline at end of file diff --git a/dist/webufse.js b/dist/webufse.js deleted file mode 100644 index 2d53ba4..0000000 --- a/dist/webufse.js +++ /dev/null @@ -1 +0,0 @@ -!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){e.exports={}}]); \ No newline at end of file diff --git a/package.json b/package.json index fbe1cf5..6bc3b26 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { "name": "webfuse", - "version": "0.0.1", + "version": "0.1.0", "description": "Webfuse JavaScript Provider", "main": "dist/webfuse.js", "scripts": { "build": "webpack --config webpack.config.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "mocha test/**/*.js" }, "repository": { "type": "git", "url": "git+https://github.com/falk-werner/webfuse-js.git" }, - "keywords": [], + "keywords": ["websockets", "libfuse", "fuse", "ws", "wss", "filesystem"], "author": "Falk Werner", "license": "MIT", "bugs": { @@ -19,7 +19,12 @@ }, "homepage": "https://github.com/falk-werner/webfuse-js#readme", "devDependencies": { + "chai": "^4.2.0", + "jsdom": "16.2.2", + "jsdom-global": "3.0.2", + "mocha": "^7.1.1", "webpack": "^4.42.1", "webpack-cli": "^3.3.11" - } + }, + "dependencies": {} } diff --git a/src/bad_state.js b/src/bad_state.js new file mode 100644 index 0000000..fdb05d9 --- /dev/null +++ b/src/bad_state.js @@ -0,0 +1,15 @@ +export class BadState extends Error { + static get BAD() { return 1; } + + static get NOT_IMPLEMENTED() { return 2; } + static get TIMEOUT() { return 3; } + static get FORMAT() { return 4; } + + static get NO_ENTRY() { return 101; } + static get NO_ACCESS() { return 102; } + + constructor(code) { + super("Bad State"); + this.code = code; + } +} \ No newline at end of file diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..b046b3f --- /dev/null +++ b/src/client.js @@ -0,0 +1,223 @@ +import { BadState } from "./bad_state.js"; + +export class Client { + static get _PROTOCOL() { return "webfuse-adapter-server"; } + + constructor(provider) { + this._provider = { }; + this._pendingRequests = {}; + this._id = 0; + this._ws = null; + this.onopen = () => { }; + this.onclose = () => { }; + this.onerror = () => { }; + } + + connectTo(url) { + this.disconnect(); + + this._ws = new WebSocket(url, Client._PROTOCOL); + this._ws.onopen = this.onopen; + this._ws.onclose = this.onclose; + this._ws.onerror = this.onerror; + + this._ws.onmessage = (message) => { + this._onmessage(message); + }; + } + + _invokeRequest(method, params) { + this._id += 1; + const id = this._id; + const request = {method, params, id}; + + return new Promise((resolve, reject) => { + this._pendingRequests[id] = {resolve, reject}; + this._ws.send(JSON.stringify(request)); + }); + } + + authenticate(type, credentials) { + return this._invokeRequest("authenticate", [type, credentials]); + } + + addProvider(name, provider) { + this._provider[name] = provider; + const request = { + "method": "add_filesystem", + "params": [name], + "id": 23 + }; + + this._ws.send(JSON.stringify(request)); + } + + disconnect() { + if (this._ws) { + this._ws.close(); + this._ws = null; + } + } + + isConnected() { + return ((this._ws) && (this._ws.readyState === WebSocket.OPEN)); + } + + _isRequest(request) { + const method = request.method; + + return (("string" === typeof(method)) && ("params" in request)); + } + + _isResponse(response) { + const id = response.id; + + return (("number" === typeof(id)) && (("result" in response) || ("error" in response))); + } + + _removePendingRequest(id) { + let result = null; + + if (id in this._pendingRequests) { + result = this._pendingRequests[id]; + Reflect.deleteProperty(this._pendingRequests, id); + } + + return result; + } + + _onmessage(message) { + try { + const data = JSON.parse(message.data); + + if (this._isRequest(data)) { + const method = data.method; + const id = data.id; + const params = data.params; + + if ("number" === typeof(id)) { + this._invoke(method, params, id); + } + else { + this._notify(method, params); + } + } + else if (this._isResponse(data)) { + const id = data.id; + const result = data.result; + const error = data.error; + + const request = this._removePendingRequest(id); + if (request) { + if (result) { + request.resolve(result); + } + else { + request.reject(error); + } + } + } + + } + catch (ex) { + // swallow + } + } + + _invoke(method, params, id) { + this._invokeAsync(method, params). + then((result) => { + const response = { result, id }; + this._ws.send(JSON.stringify(response)); + }). + catch((ex) => { + const code = ex.code || BadState.BAD; + const response = {error: {code}, id}; + this._ws.send(JSON.stringify(response)); + }); + + } + + async _invokeAsync(method, params) { + switch(method) + { + case "lookup": + return this._lookup(params); + case "getattr": + return this._getattr(params); + case "readdir": + return this._readdir(params); + case "open": + return this._open(params); + case "read": + return this._read(params); + default: + throw new BadState(BadState.NOT_IMPLEMENTED); + } + } + + _notify(method, params) { + switch(method) { + case 'close': + this._close(params); + break; + default: + throw new Error(`Invalid method: "${method}"`); + } + } + + _getProvider(name) { + if (name in this._provider) { + return this._provider[name]; + } + else { + throw new Error('Unknown provider'); + } + } + + async _lookup([providerName, parent, name]) { + const provider = this._getProvider(providerName); + + return provider.lookup(parent, name); + } + + async _getattr([providerName, inode]) { + const provider = this._getProvider(providerName); + + return provider.getattr(inode); + } + + async _readdir([providerName, inode]) { + const provider = this._getProvider(providerName); + + return provider.readdir(inode); + } + + async _open([providerName, inode, mode]) { + const provider = this._getProvider(providerName); + + return provider.open(inode, mode); + } + + _close([providerName, inode, handle, mode]) { + const provider = this._getProvider(providerName); + + provider.close(inode, handle, mode); + } + + async _read([providerName, inode, handle, offset, length]) { + const provider = this._getProvider(providerName); + const data = await provider.read(inode, handle, offset, length); + + if ("string" === typeof(data)) { + return { + data: btoa(data), + format: "base64", + count: data.length + }; + } + else { + throw new BadState(BadState.BAD); + } + } +} \ No newline at end of file diff --git a/src/file_mode.js b/src/file_mode.js new file mode 100644 index 0000000..b81a1ea --- /dev/null +++ b/src/file_mode.js @@ -0,0 +1,10 @@ +export class FileMode { + static get ACCESS_MODE() { return 0x003; } + static get READONLY() { return 0x000; } + static get WRITEONLY() { return 0x001; } + static get READWRITE() { return 0x002; } + static get CREATE() { return 0x040; } + static get EXCLUSIVE() { return 0x080; } + static get TRUNKATE() { return 0x200; } + static get APPEND() { return 0x400; } +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index d7b8821..818a02b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,8 @@ +import { BadState } from './bad_state.js' +import { Provider } from './provider.js' +import { FileMode } from './file_mode.js' +import { Client } from './client.js' -module.exports = { - +export { + BadState, Provider, FileMode, Client }; \ No newline at end of file diff --git a/src/provider.js b/src/provider.js new file mode 100644 index 0000000..b9c5dc2 --- /dev/null +++ b/src/provider.js @@ -0,0 +1,30 @@ +/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ + +import { BadState } from "./bad_state.js"; + +export class Provider { + + async lookup(_parent, _name) { + throw new BadState(BadState.NOT_IMPLEMENTED); + } + + async getattr(_inode) { + throw new BadState(BadState.NOT_IMPLEMENTED); + } + + async readdir(_inode) { + throw new BadState(BadState.NOT_IMPLEMENTED); + } + + async open(_inode, _mode) { + throw new BadState(BadState.NOT_IMPLEMENTED); + } + + close(_inode, _handle, _mode) { + // empty + } + + async read(_inode, _handle, _offset, _length) { + throw new BadState(BadState.NOT_IMPLEMENTED); + } +} diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..df2a0f2 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +-r jsdom-global/register \ No newline at end of file diff --git a/test/test_provider.js b/test/test_provider.js new file mode 100644 index 0000000..7afe789 --- /dev/null +++ b/test/test_provider.js @@ -0,0 +1,20 @@ +const webfuse = require('../dist/webfuse') + +const chai = require('chai'); +const assert = chai.assert; + +describe('Provider', function() { + + it('lookup should throw not implemented error', function() { + const provider = new webfuse.Provider(); + + return provider.lookup(1, 'root').then( + () => Promise.reject(new Error("reject expected")), + err => { + assert.instanceOf(err, webfuse.BadState); + assert.equal(webfuse.BadState.NOT_IMPLEMENTED, err.code); + } + ); + }); + +}); \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 649c4c8..2dd4740 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,9 @@ module.exports = { mode: 'production', entry: './src/index.js', output: { - filename: 'webufse.js', + filename: 'webfuse.js', + library: 'webfuse', + libraryTarget: 'umd', path: path.resolve(__dirname, './dist') } };