From 460fedae22518a2a4752f8278bfa6ee353193016 Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Sun, 9 Feb 2020 20:24:35 +0100 Subject: [PATCH] feature: imported sources from webfuse --- .travis.yml | 8 + Dockerfile | 125 +++++++++++++++ README.md | 13 +- www/index.html | 18 +++ www/js/.eslintrc.js | 287 ++++++++++++++++++++++++++++++++++ www/js/connection_view.js | 93 +++++++++++ www/js/filesystem_provider.js | 122 +++++++++++++++ www/js/package.json | 11 ++ www/js/startup.js | 25 +++ www/js/webfuse/bad_state.js | 15 ++ www/js/webfuse/client.js | 223 ++++++++++++++++++++++++++ www/js/webfuse/file_mode.js | 10 ++ www/js/webfuse/provider.js | 30 ++++ www/style/main.css | 47 ++++++ 14 files changed, 1026 insertions(+), 1 deletion(-) create mode 100644 .travis.yml create mode 100644 Dockerfile create mode 100644 www/index.html create mode 100644 www/js/.eslintrc.js create mode 100644 www/js/connection_view.js create mode 100644 www/js/filesystem_provider.js create mode 100644 www/js/package.json create mode 100644 www/js/startup.js create mode 100644 www/js/webfuse/bad_state.js create mode 100644 www/js/webfuse/client.js create mode 100644 www/js/webfuse/file_mode.js create mode 100644 www/js/webfuse/provider.js create mode 100644 www/style/main.css diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..be1e184 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +dist: bionic + +services: + - docker + +script: + - docker build --rm --buildarg "USERID=`id -u`" -tag webfuse . + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..52e5af1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,125 @@ +ARG REGISTRY_PREFIX='' +ARG CODENAME=bionic + +FROM ${REGISTRY_PREFIX}ubuntu:${CODENAME} as builder + +RUN set -x \ + && apt update \ + && apt upgrade -y \ + && apt install --yes --no-install-recommends \ + build-essential \ + cmake \ + ninja-build \ + pkg-config \ + ca-certificates \ + openssl \ + libssl-dev \ + uuid-dev \ + wget + +COPY www /var/www + +ARG PARALLELMFLAGS=-j2 + +ARG DUMB_INIT_VERSION=1.2.2 +RUN set -x \ + && builddeps="xxd" \ + && apt install --yes --no-install-recommends $builddeps \ + && builddir="/tmp/out" \ + && mkdir -p "$builddir" \ + && cd "$builddir" \ + && wget "https://github.com/Yelp/dumb-init/archive/v${DUMB_INIT_VERSION}.tar.gz" -O dumb_init.tar.gz \ + && tar -xf dumb_init.tar.gz \ + && cd "dumb-init-$DUMB_INIT_VERSION" \ + && make "$PARALLELMFLAGS" \ + && chmod +x dumb-init \ + && mv dumb-init /usr/local/bin/dumb-init \ + && dumb-init --version \ + && rm -rf "$builddir" \ + && apt purge -y $builddeps + +ARG FUSE_VERSION=3.9.0 +RUN set -x \ + && builddeps="udev gettext python3 python3-pip python3-setuptools python3-wheel" \ + && apt install --yes --no-install-recommends $builddeps \ + && pip3 install --system meson \ + && builddir="/tmp/out" \ + && mkdir -p "$builddir" \ + && cd "$builddir" \ + && wget "https://github.com/libfuse/libfuse/archive/fuse-${FUSE_VERSION}.tar.gz" -O libfuse.tar.gz \ + && tar -xf libfuse.tar.gz \ + && cd "libfuse-fuse-$FUSE_VERSION" \ + && mkdir .build \ + && cd .build \ + && meson .. \ + && ninja \ + && ninja install \ + && pip3 uninstall -y meson \ + && rm -rf "$builddir" \ + && apt purge -y $builddeps + +ARG WEBSOCKETS_VERSION=3.2.0 +RUN set -x \ + && apt install --yes --no-install-recommends \ + ca-certificates \ + openssl \ + libssl-dev \ + && builddir="/tmp/out" \ + && mkdir -p "$builddir" \ + && cd "$builddir" \ + && wget "https://github.com/warmcat/libwebsockets/archive/v${WEBSOCKETS_VERSION}.tar.gz" -O libwebsockets.tar.gz \ + && tar -xf libwebsockets.tar.gz \ + && cd "libwebsockets-$WEBSOCKETS_VERSION" \ + && mkdir .build \ + && cd .build \ + && cmake .. \ + && make "$PARALLELMFLAGS" install \ + && rm -rf "$builddir" + +ARG JANSSON_VERSION=2.12 +RUN set -x \ + && builddir="/tmp/out" \ + && mkdir -p "$builddir" \ + && cd "$builddir" \ + && wget "https://github.com/akheron/jansson/archive/v${JANSSON_VERSION}.tar.gz" -O jansson.tar.gz \ + && tar -xf jansson.tar.gz \ + && cd "jansson-$JANSSON_VERSION" \ + && mkdir .build \ + && cd .build \ + && cmake -DJANSSON_BUILD_DOCS=OFF ".." \ + && make "$PARALLELMFLAGS" install \ + && rm -rf "$builddir" + +ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib" + +ARG WEBFUSE_VERSION=master +RUN set -x \ + && builddir="/tmp/out" \ + && mkdir -p "$builddir" \ + && cd "$builddir" \ + && wget "https://github.com/falk-werner/webfuse/archive/${WEBFUSE_VERSION}.tar.gz" -O webfuse.tar.gz \ + && tar -xf webfuse.tar.gz \ + && cd "webfuse-$WEBFUSE_VERSION" \ + && mkdir .build \ + && cd .build \ + && cmake -DWITHOUT_TESTS=ON -DWITHOUT_EXAMPLE=ON ".." \ + && make "$PARALLELMFLAGS" install \ + && rm -rf "$builddir" + +ARG WEBFUSED_VERSION=master +RUN set -x \ + && builddir="/tmp/out" \ + && mkdir -p "$builddir" \ + && cd "$builddir" \ + && wget "https://github.com/falk-werner/webfused/archive/${WEBFUSED_VERSION}.tar.gz" -O webfused.tar.gz \ + && tar -xf webfused.tar.gz \ + && cd "webfused-$WEBFUSED_VERSION" \ + && mkdir .build \ + && cd .build \ + && cmake ".." \ + && make "$PARALLELMFLAGS" install \ + && rm -rf "$builddir" + +EXPOSE 8080 + +ENTRYPOINT ["dumb-init", "--"] \ No newline at end of file diff --git a/README.md b/README.md index faa9ff3..2043e2b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ # webfuse-example -Example of webfuse +Example of webfuse. + +## Build + + docker build --rm --buildarg "USERID=`id -u`" -tag webfuse . + +# Run + + docker run -p 8080:8080 --rm -it --user "`id -u`" webfuse bash + webfused -m /tmp -d /var/www -p 8080 + +Open a webbrowser and visit http://localhost:8080. \ No newline at end of file diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..0cb0918 --- /dev/null +++ b/www/index.html @@ -0,0 +1,18 @@ + + + WebFuse Example + + + + + + +
+
+
Connection
+
+
+
+ + + \ No newline at end of file diff --git a/www/js/.eslintrc.js b/www/js/.eslintrc.js new file mode 100644 index 0000000..9090e7d --- /dev/null +++ b/www/js/.eslintrc.js @@ -0,0 +1,287 @@ +module.exports = { + "env": { + "browser": true, + "es6": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "rules": { + "accessor-pairs": "error", + "array-bracket-newline": "error", + "array-bracket-spacing": "error", + "array-callback-return": "error", + "array-element-newline": ["error", "consistent"], + "arrow-body-style": "error", + "arrow-parens": [ + "error", + "always" + ], + "arrow-spacing": [ + "error", + { + "after": true, + "before": true + } + ], + "block-scoped-var": "error", + "block-spacing": [ + "error", + "always" + ], + "brace-style": "off", + "callback-return": "off", + "camelcase": "error", + "capitalized-comments": [ + "error", + "never" + ], + "class-methods-use-this": "off", + "comma-dangle": "error", + "comma-spacing": [ + "error", + { + "after": true, + "before": false + } + ], + "comma-style": [ + "error", + "last" + ], + "complexity": "error", + "computed-property-spacing": [ + "error", + "never" + ], + "consistent-return": "error", + "consistent-this": "error", + "curly": "error", + "default-case": "error", + "dot-location": "error", + "dot-notation": "error", + "eol-last": "off", + "eqeqeq": "off", + "func-call-spacing": "error", + "func-name-matching": "error", + "func-names": "error", + "func-style": [ + "error", + "declaration" + ], + "function-paren-newline": "error", + "generator-star-spacing": "error", + "global-require": "error", + "guard-for-in": "error", + "handle-callback-err": "error", + "id-blacklist": "error", + "id-length": "error", + "id-match": "error", + "implicit-arrow-linebreak": [ + "error", + "beside" + ], + "indent": "off", + "indent-legacy": "off", + "init-declarations": "off", + "jsx-quotes": "error", + "key-spacing": "off", + "keyword-spacing": "off", + "line-comment-position": "error", + "linebreak-style": [ + "error", + "unix" + ], + "lines-around-comment": "error", + "lines-around-directive": "error", + "lines-between-class-members": "off", + "max-classes-per-file": "error", + "max-depth": "error", + "max-len": "off", + "max-lines": "error", + "max-lines-per-function": "off", + "max-nested-callbacks": "error", + "max-params": "off", + "max-statements": "off", + "max-statements-per-line": "off", + "multiline-comment-style": "error", + "new-cap": "error", + "new-parens": "error", + "newline-after-var": "off", + "newline-before-return": "error", + "newline-per-chained-call": "error", + "no-alert": "error", + "no-array-constructor": "error", + "no-async-promise-executor": "error", + "no-await-in-loop": "error", + "no-bitwise": "off", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-catch-shadow": "error", + "no-confusing-arrow": "error", + "no-continue": "error", + "no-div-regex": "error", + "no-duplicate-imports": "error", + "no-else-return": "off", + "no-empty-function": "off", + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-label": "error", + "no-extra-parens": "off", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implicit-globals": "off", + "no-implied-eval": "error", + "no-inline-comments": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-lonely-if": "error", + "no-loop-func": "error", + "no-magic-numbers": "off", + "no-misleading-character-class": "error", + "no-mixed-operators": "error", + "no-mixed-requires": "error", + "no-multi-assign": "error", + "no-multi-spaces": "off", + "no-multi-str": "error", + "no-multiple-empty-lines": "error", + "no-native-reassign": "error", + "no-negated-condition": "off", + "no-negated-in-lhs": "error", + "no-nested-ternary": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-object": "error", + "no-new-require": "error", + "no-new-wrappers": "error", + "no-octal-escape": "error", + "no-param-reassign": "error", + "no-path-concat": "error", + "no-plusplus": "error", + "no-process-env": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "error", + "no-restricted-globals": "error", + "no-restricted-imports": "error", + "no-restricted-modules": "error", + "no-restricted-properties": "error", + "no-restricted-syntax": "error", + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "off", + "no-shadow-restricted-names": "error", + "no-spaced-func": "error", + "no-sync": "error", + "no-tabs": "off", + "no-template-curly-in-string": "error", + "no-ternary": "off", + "no-throw-literal": "error", + "no-trailing-spaces": "off", + "no-undef-init": "error", + "no-undefined": "error", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": "error", + "no-unused-expressions": "error", + "no-use-before-define": "error", + "no-useless-call": "error", + // "no-useless-catch": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-var": "error", + "no-void": "error", + "no-warning-comments": "error", + "no-whitespace-before-property": "error", + "no-with": "error", + "nonblock-statement-body-position": "error", + "object-curly-newline": "error", + "object-curly-spacing": "off", + "object-shorthand": "off", + "one-var": "off", + "one-var-declaration-per-line": "error", + "operator-assignment": "error", + "operator-linebreak": "error", + "padded-blocks": "off", + "padding-line-between-statements": "error", + "prefer-arrow-callback": "error", + "prefer-const": "off", + "prefer-destructuring": "off", + "prefer-numeric-literals": "off", + "prefer-object-spread": "error", + "prefer-promise-reject-errors": "error", + "prefer-reflect": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "quote-props": "off", + "quotes": "off", + "radix": "error", + "require-atomic-updates": "error", + "require-await": "off", + "require-jsdoc": "off", + "require-unicode-regexp": "off", + "rest-spread-spacing": "error", + "semi": "error", + "semi-spacing": "error", + "semi-style": [ + "error", + "last" + ], + "sort-imports": "error", + "sort-keys": "off", + "sort-vars": "error", + "space-before-blocks": "error", + "space-before-function-paren": "off", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-unary-ops": [ + "error", + { + "nonwords": false, + "words": false + } + ], + "spaced-comment": [ + "error", + "always" + ], + "strict": [ + "error", + "never" + ], + "switch-colon-spacing": "error", + "symbol-description": "error", + "template-curly-spacing": "error", + "template-tag-spacing": "error", + "unicode-bom": [ + "error", + "never" + ], + "valid-jsdoc": "error", + "vars-on-top": "error", + "wrap-iife": "error", + "wrap-regex": "error", + "yield-star-spacing": "error", + "yoda": "off" + } +}; \ No newline at end of file diff --git a/www/js/connection_view.js b/www/js/connection_view.js new file mode 100644 index 0000000..31576ae --- /dev/null +++ b/www/js/connection_view.js @@ -0,0 +1,93 @@ +export class ConnectionView { + constructor(client, provider) { + this._provider = provider; + this._client = client; + this._client.onopen = () => { this._onConnectionOpened(); }; + this._client.onclose = () => { this._onConnectionClosed(); }; + + this.element = document.createElement("div"); + + const connectBox = document.createElement("div"); + this.element.appendChild(connectBox); + + const urlLabel = document.createElement("span"); + urlLabel.textContent = "URL:"; + connectBox.appendChild(urlLabel); + + this.urlTextbox = document.createElement("input"); + this.urlTextbox.type = "text"; + this.urlTextbox.value = window.location.href.replace(/^http/, "ws"); + connectBox.appendChild(this.urlTextbox); + + this.connectButton = document.createElement("input"); + this.connectButton.type = "button"; + this.connectButton.value = "connect"; + this.connectButton.addEventListener("click", () => { this._onConnectButtonClicked(); }); + connectBox.appendChild(this.connectButton); + + + const authenticateBox = document.createElement("div"); + this.element.appendChild(authenticateBox); + + const authLabel = document.createElement("span"); + authLabel.textContent = "use authentication:"; + authenticateBox.appendChild(authLabel); + + this.authenticateCheckbox = document.createElement("input"); + this.authenticateCheckbox.type = "checkbox"; + authenticateBox.appendChild(this.authenticateCheckbox); + + const usernameLabel = document.createElement("span"); + usernameLabel.textContent = "user:"; + authenticateBox.appendChild(usernameLabel); + + this.usernameTextbox = document.createElement("input"); + this.usernameTextbox.type = "text"; + this.usernameTextbox.value = "bob"; + authenticateBox.appendChild(this.usernameTextbox); + + const passwordLabel = document.createElement("span"); + passwordLabel.textContent = "user:"; + authenticateBox.appendChild(passwordLabel); + + this.passwordTextbox = document.createElement("input"); + this.passwordTextbox.type = "password"; + this.passwordTextbox.value = "secret"; + authenticateBox.appendChild(this.passwordTextbox); + } + + _onConnectButtonClicked() { + if (!this._client.isConnected()) { + let url = this.urlTextbox.value; + this._client.connectTo(url); + } + else { + this._client.disconnect(); + } + } + + _onAuthenticateButtonClicked() { + if (this._client.isConnected()) { + + } + } + + _onConnectionOpened() { + if (this.authenticateCheckbox.checked) { + const username = this.usernameTextbox.value; + const password = this.passwordTextbox.value; + + const promise = this._client.authenticate("username", { username, password }); + promise.then(() => { this._client.addProvider("test", this._provider); }); + } else { + this._client.addProvider("test", this._provider); + } + + this.connectButton.value = "disconnect"; + } + + _onConnectionClosed() { + this.connectButton.value = "connect"; + } + +} diff --git a/www/js/filesystem_provider.js b/www/js/filesystem_provider.js new file mode 100644 index 0000000..85ad287 --- /dev/null +++ b/www/js/filesystem_provider.js @@ -0,0 +1,122 @@ +/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ + +import { BadState } from "./webfuse/bad_state.js"; +import { FileMode } from "./webfuse/file_mode.js"; +import { Provider } from "./webfuse/provider.js"; + +export class FileSystemProvider extends Provider { + constructor(root) { + super(); + + this.root = root; + this._inodes = { }; + + this._walk(this.root, (entry) => { this._inodes[entry.inode] = entry; }); + } + + _walk(node, callback) { + callback(node); + + const entries = node.entries; + if (entries) { + for(let entry of Object.entries(entries)) { + this._walk(entry[1], callback); + } + } + } + + + async lookup(parent, name) { + const parentEntry = this._inodes[parent]; + const entry = (parentEntry && parentEntry.entries && parentEntry.entries[name]) || null; + if (entry) { + return { + inode: entry.inode, + mode: entry.mode || parseInt("755", 8), + type: entry.type || "file", + size: entry.size || (entry.contents && entry.contents.length) || 0, + atime: entry.atime || 0, + mtime: entry.mtime || 0, + ctime: entry.ctime || 0 + }; + } + else { + throw new BadState(BadState.NO_ENTRY); + } + } + + + async getattr(inode) { + let entry = this._inodes[inode]; + if (entry) { + return { + mode: entry.mode || parseInt("755", 8), + type: entry.type || "file", + size: entry.size || (entry.contents && entry.contents.length) || 0, + atime: entry.atime || 0, + mtime: entry.mtime || 0, + ctime: entry.ctime || 0 + }; + } + else { + throw new BadState(BadState.NO_ENTRY); + } + } + + async readdir(inode) { + let entry = this._inodes[inode]; + + if ((entry) && ("dir" === entry.type)) { + let result = [ + {name: ".", inode: entry.inode}, + {name: "..", inode: entry.inode} + ]; + for(let subdir of Object.entries(entry.entries)) { + const name = subdir[0]; + const inode = subdir[1].inode; + result.push({name, inode}); + } + + return result; + } + else { + throw new BadState(BadState.NO_ENTRY); + } + + } + + async open(inode, mode) { + let entry = this._inodes[inode]; + + if (entry.type === "file") { + if ((mode & FileMode.ACCESS_MODE) === FileMode.READONLY) { + return {handle: 1337}; + } + else { + throw new BadState(BadState.NO_ACCESS); + } + } + else { + throw new BadState(BadState.NO_ENTRY); + } + } + + close(_inode, _handle, _mode) { + // do nothing + return true; + } + + async read(inode, handle, offset, length) { + let entry = this._inodes[inode]; + + if (entry.type === "file") { + const end = Math.min(offset + length, entry.contents.length); + const data = (offset < entry.contents.length) ? entry.contents.substring(offset, end) : ""; + + return data; + } + else { + throw new BadState(BadState.NO_ENTRY); + } + } +} \ No newline at end of file diff --git a/www/js/package.json b/www/js/package.json new file mode 100644 index 0000000..6c553ef --- /dev/null +++ b/www/js/package.json @@ -0,0 +1,11 @@ +{ + "name": "webfuse-provider", + "version": "0.2.0", + "description": "Provider for websocket filesystem (webfuse)", + "main": "startup.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Falk Werner", + "license": "LGPL-3.0" +} diff --git a/www/js/startup.js b/www/js/startup.js new file mode 100644 index 0000000..284d229 --- /dev/null +++ b/www/js/startup.js @@ -0,0 +1,25 @@ +import { Client } from "./webfuse/client.js"; +import { ConnectionView } from "./connection_view.js"; +import { FileSystemProvider } from "./filesystem_provider.js"; + + +function mode(value) { + return parseInt(value, 8); +} + +function startup() { + const provider = new FileSystemProvider({ + inode: 1, + mode: mode("0755"), + type: "dir", + entries: { + "hello.txt" : { inode: 2, mode: mode("0444"), type: "file", contents: "Hello, World!"}, + "say_hello.sh": { inode: 3, mode: mode("0555"), type: "file", contents: "#!/bin/sh\necho hello\n"} + } + }); + const client = new Client(); + const connectionView = new ConnectionView(client, provider); + document.getElementById('connection').appendChild(connectionView.element); +} + +window.onload = startup; diff --git a/www/js/webfuse/bad_state.js b/www/js/webfuse/bad_state.js new file mode 100644 index 0000000..fdb05d9 --- /dev/null +++ b/www/js/webfuse/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/www/js/webfuse/client.js b/www/js/webfuse/client.js new file mode 100644 index 0000000..65d24f2 --- /dev/null +++ b/www/js/webfuse/client.js @@ -0,0 +1,223 @@ +import { BadState } from "./bad_state.js"; + +export class Client { + static get _PROTOCOL() { return "fs"; } + + 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/www/js/webfuse/file_mode.js b/www/js/webfuse/file_mode.js new file mode 100644 index 0000000..b81a1ea --- /dev/null +++ b/www/js/webfuse/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/www/js/webfuse/provider.js b/www/js/webfuse/provider.js new file mode 100644 index 0000000..b9c5dc2 --- /dev/null +++ b/www/js/webfuse/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/www/style/main.css b/www/style/main.css new file mode 100644 index 0000000..399bfd1 --- /dev/null +++ b/www/style/main.css @@ -0,0 +1,47 @@ +html, body { + font-family: monospace; + background-color: #c0c0c0; +} + +.page { + margin-left: 50px; + margin-right: 50px; + width: auto; +} + +.window { + border: 1px solid black; + background-color: black; + border-radius: 5px; + padding: 10px; + margin-bottom: 25px; + color: white; +} + +.window .title { + text-align: center; + color: #dba329; + font-weight: bold; + padding-bottom: 10px; + margin-bottom: 10px; + border-bottom: 1px solid #dba329; +} + +.commands { + text-align: right; +} + +.content { + column-count: 2; + column-width: 50%; +} + +.content > div { + display: inline-block; + width: 100%; +} + +#connection { + text-align: center; +} +