feature: imported sources from webfuse

pull/1/head
Falk Werner 4 years ago
parent 288b38c7f5
commit 460fedae22

@ -0,0 +1,8 @@
dist: bionic
services:
- docker
script:
- docker build --rm --buildarg "USERID=`id -u`" -tag webfuse .

@ -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", "--"]

@ -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.

@ -0,0 +1,18 @@
<html>
<head>
<title>WebFuse Example</title>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="style/main.css">
<script type="module" src="js/startup.js"></script>
</head>
<body>
<div class="page">
<div class="window">
<div class="title">Connection</div>
<div id="connection"></div>
</div>
</div>
</body>
</html>

@ -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"
}
};

@ -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";
}
}

@ -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);
}
}
}

@ -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"
}

@ -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;

@ -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;
}
}

@ -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);
}
}
}

@ -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; }
}

@ -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);
}
}

@ -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;
}
Loading…
Cancel
Save