From d7ebb30e05da107025d3e77f246f09ded77da686 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Sat, 8 Feb 2020 16:26:29 +0100 Subject: [PATCH] servers/plugin-state --- README.md | 1 + package-lock.json | Bin 764447 -> 765664 bytes package.json | 3 + src/servers/model/server/api-local.ts | 15 +- src/servers/plugin-state/index.ts | 242 ++++++++++++++++++++++++++ 5 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 src/servers/plugin-state/index.ts diff --git a/README.md b/README.md index d5dd8c109..91d48e5e5 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Moreover, the project contains the imlementation of `servers`, including - `servers/model` A tool for accessing coordinate and annotation data of molecular structures. - `servers/volume` A tool for accessing volumetric experimental data related to molecular structures. +- `servers/plugin-state` A basic server to store Mol* Plugin states. The project also contains performance tests (`perf-tests`), `examples`, and basic proof of concept `apps` (CIF to BinaryCIF converter and JSON domain annotation to CIF converter). diff --git a/package-lock.json b/package-lock.json index 3cc465afa6de76dc269c4ad9dab41dd3bfd21d0c..9c313aac8c467562d01a37da6bac6b63406507e1 100644 GIT binary patch delta 711 zcmZWkUr1AN6z1OFJ?(C0twAug2a}9I=G|<1UHITO-Q9F;>gIGp(*EhDb90+pv$D*B zK@bR%M*}UXm!JoW;wKuFJ@gj6)I)?NJw*>;y+jb%-0iK0kMo`L@qOo<+i&Z)x9X>_ zF^}2xWfDrJ=N^&@(oo-wM;E||)hSkSusF>KI$^Kq_7w9isO^m<a_+HYreEKd3XYrW zLhTKcNxRe=w+X@c<dB%K=-ravVu{%s#85iq8*6lp*#ZNj&Mu=jV2DbANG3FDjWx!7 z&iG{5)nUz#dfgseS!G-C^<7qhJxgpYU0-ImzhLVusi7MMG6*s~b%Pv>%snDn^t=P{ zzw->gisyAY>_KNoDl4Ask<vmm5lp*HSxY3AcS+ftr+XwD7e>OioJZ)ic^q~_w9gkd zB!<QFZhOAYpKc#=B%G3%GA5cNd$%dnHj&Q>T}}A?9$Sn0PF{h(m-zkjAIV{65Y4Yt zm2}_-*)7M}n|#HS6t~8d7&Wt;S;T2Gr!6d+IJ#(pZ<VMX1S4Jc0?U>t?i>QZr#?tx z>ltXKwm$GOva&Wq%JEr0c;xotRh~z^A0l$Z13zq($@EeLc&22>3tyEjxHSZS<hHtn zDTS9KABF33&iyFF<z4z1tdx8$bY}#P^RlkXz^?3m9|k==Hv!W;j!!`oRZoGF$kK;R zg7m-@aQ)l69Z&$yE;2kmKf>!U`H}#B%7Yr65tWj@nE{5A(@&du6<$6;s_@P#qnS^F z71drSKJt^%7KRH@#J(aFMeuaEptKI)*&4P2UxaxL-ga;^)c0A50#<JlZGqFMD5oi_ GTKEf^qwz!l delta 543 zcmXX@OK1~O6y?pEo6by~cFZD4O`<7<fR)A({7{Kf6hshd!D4ZWcC-f48Z{vJS!k!A zj*+$$ialaM>%y(N5FfgbLg=D`)rH`yB_OB^rS-E?sJ_g&IB>Y<+<VXE)|PS)-simQ z?3wcNzR+it+86YswAk|sAEJN>Z-Q?vh@ZdsAG2PFiwIX=u#kRG)qT4Hn{Ve;VdrUO z18iss(c4E%LG7)Y!o)E;KUIz<#SVvh8EO~Q1YY&iK**98sRiz5u4AM_S&yE6>I{X2 zYt)3LA5zG<6oq?*O6WHAR!q*23h#|#;ax%2R+rf};hdl&2CQ#-i}+Zi2t#H?@5Aw7 zTB~nHCeNsz!$f3Av+z5?(r9{Yw848QXC-f{7VMLh#j38vF<7Ef(6y;TJH?)}bcUg8 zoTaDoSwnn1M;o=!xAzkL4Shwj#iPsQ1wukNqjZ>0R~ypERrwu^kI22<rwko)zF|#w z@@BY4=^!R=Q#&#hd8IdKG~-FJF$H%<+g*>c9p%@&9bT)p1#?wSFt3sYb65^pJj9|X zcPY`j&p}~S_O9JymiX?|X&y#fcR(|+xS1z0`&7Dv+q_%6&CiaJL`0%NLKE3BeZTYn W&!6-T8<`tg+@FsbVm{WGy!#IaLbv<? diff --git a/package.json b/package.json index f6d8ef285..9f4a69ce9 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "model-server": "node lib/servers/model/server.js", "model-server-watch": "nodemon --watch lib lib/servers/model/server.js", "volume-server": "node lib/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336", + "plugin-state": "node lib/servers/plugin-state/index.js", "preversion": "npm run test", "postversion": "git push && git push --tags", "prepublishOnly": "npm run test && npm run build" @@ -70,6 +71,7 @@ "@graphql-codegen/typescript-graphql-files-modules": "^1.11.2", "@graphql-codegen/typescript-graphql-request": "^1.11.2", "@graphql-codegen/typescript-operations": "^1.11.2", + "@types/cors": "^2.8.6", "@typescript-eslint/eslint-plugin": "^2.17.0", "@typescript-eslint/eslint-plugin-tslint": "^2.17.0", "@typescript-eslint/parser": "^2.17.0", @@ -112,6 +114,7 @@ "argparse": "^1.0.10", "body-parser": "^1.19.0", "compression": "^1.7.4", + "cors": "^2.8.5", "express": "^4.17.1", "graphql": "^14.5.8", "immutable": "^3.8.2", diff --git a/src/servers/model/server/api-local.ts b/src/servers/model/server/api-local.ts index 5b27b9859..31aca674e 100644 --- a/src/servers/model/server/api-local.ts +++ b/src/servers/model/server/api-local.ts @@ -13,6 +13,7 @@ import { StructureCache } from './structure-wrapper'; import { now } from '../../../mol-util/now'; import { PerformanceMonitor } from '../../../mol-util/performance-monitor'; import { QueryName } from './api'; +import { makeDir } from '../../../mol-util/make-dir'; export type LocalInput = { input: string, @@ -103,18 +104,4 @@ export function wrapFileToWriter(fn: string) { }; return w; -} - -function makeDir(path: string, root?: string): boolean { - let dirs = path.split(/\/|\\/g), - dir = dirs.shift(); - - root = (root || '') + dir + '/'; - - try { fs.mkdirSync(root); } - catch (e) { - if (!fs.statSync(root).isDirectory()) throw new Error(e); - } - - return !dirs.length || makeDir(dirs.join('/'), root); } \ No newline at end of file diff --git a/src/servers/plugin-state/index.ts b/src/servers/plugin-state/index.ts new file mode 100644 index 000000000..0c37c4959 --- /dev/null +++ b/src/servers/plugin-state/index.ts @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as express from 'express' +import * as compression from 'compression' +import * as cors from 'cors' +import * as bodyParser from 'body-parser' +import * as argparse from 'argparse' +import * as fs from 'fs' +import * as path from 'path' +import { makeDir } from '../../mol-util/make-dir' + +interface Config { + working_folder: string, + port?: string | number, + app_prefix: string, + max_states: number +} + +const cmdParser = new argparse.ArgumentParser({ + addHelp: true +}); +cmdParser.addArgument(['--working-folder'], { help: 'Working forlder path.', required: true }); +cmdParser.addArgument(['--port'], { help: 'Server port. Altenatively use ENV variable PORT.', type: 'int', required: false }); +cmdParser.addArgument(['--app-prefix'], { help: 'Server app prefix.', defaultValue: '', required: false }); +cmdParser.addArgument(['--max-states'], { help: 'Maxinum number of states that could be saved.', defaultValue: 40, type: 'int', required: false }); + +const Config = cmdParser.parseArgs() as Config; + +if (!Config.port) Config.port = process.env.port || 1339; + +const app = express(); +app.use(compression(<any>{ level: 6, memLevel: 9, chunkSize: 16 * 16384, filter: () => true })); +app.use(cors({ methods: ['GET', 'PUT'] })); +app.use(bodyParser.json({ limit: '20mb' })); + +type Index = { timestamp: number, id: string, name: string, description: string, isSticky?: boolean }[] + +function createIndex() { + const fn = path.join(Config.working_folder, 'index.json'); + if (fs.existsSync(fn)) return; + if (!fs.existsSync(Config.working_folder)) makeDir(Config.working_folder); + fs.writeFileSync(fn, '[]', 'utf-8'); +} + +function writeIndex(index: Index) { + const fn = path.join(Config.working_folder, 'index.json'); + if (!fs.existsSync(Config.working_folder)) makeDir(Config.working_folder); + fs.writeFileSync(fn, JSON.stringify(index, null, 2), 'utf-8'); +} + +function readIndex() { + const fn = path.join(Config.working_folder, 'index.json'); + if (!fs.existsSync(fn)) return []; + return JSON.parse(fs.readFileSync(fn, 'utf-8')) as Index; +} + +function validateIndex(index: Index) { + if (index.length > Config.max_states) { + const deletes: Index = [], newIndex: Index = []; + const toDelete = index.length - Config.max_states; + + for (const e of index) { + if (!e.isSticky && deletes.length < toDelete) { + deletes.push(e); + } else { + newIndex.push(e); + } + } + // index.slice(0, index.length - 30); + for (const d of deletes) { + try { + fs.unlinkSync(path.join(Config.working_folder, d.id + '.json')) + } catch { } + } + return newIndex; + } + return index; +} + +function remove(id: string) { + let index = readIndex(); + let i = 0; + for (const e of index) { + if (e.id !== id) { + i++; + continue; + } + try { + for (let j = i + 1; j < index.length; j++) { + index[j - 1] = index[j]; + } + index.pop(); + writeIndex(index); + } catch { } + try { + fs.unlinkSync(path.join(Config.working_folder, e.id + '.json')) + } catch { } + return; + } +} + +function clear() { + let index = readIndex(); + for (const e of index) { + try { + fs.unlinkSync(path.join(Config.working_folder, e.id + '.json')) + } catch { } + } + writeIndex([]); +} + +export function createv4() { + let d = (+new Date()); + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = (d + Math.random()*16)%16 | 0; + d = Math.floor(d/16); + return (c==='x' ? r : (r&0x3|0x8)).toString(16); + }); + return uuid.toLowerCase(); +} + +function mapPath(path: string) { + if (!Config.app_prefix) return path; + return `/${Config.app_prefix}/${path}`; +} + +app.get(mapPath(`/get/:id`), (req, res) => { + const id: string = req.params.id || ''; + console.log('Reading', id); + if (id.length === 0 || id.indexOf('.') >= 0 || id.indexOf('/') >= 0 || id.indexOf('\\') >= 0) { + res.status(404); + res.end(); + return; + } + + fs.readFile(path.join(Config.working_folder, id + '.json'), 'utf-8', (err, data) => { + if (err) { + res.status(404); + res.end(); + return; + } + + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + }); + res.write(data); + res.end(); + }); +}); + +app.get(mapPath(`/clear`), (req, res) => { + clear(); + res.status(200); + res.end(); +}); + +app.get(mapPath(`/remove/:id`), (req, res) => { + remove((req.params.id as string || '').toLowerCase()); + res.status(200); + res.end(); +}); + +app.get(mapPath(`/latest`), (req, res) => { + const index = readIndex(); + const id: string = index.length > 0 ? index[index.length - 1].id : ''; + console.log('Reading', id); + if (id.length === 0 || id.indexOf('.') >= 0 || id.indexOf('/') >= 0 || id.indexOf('\\') >= 0) { + res.status(404); + res.end(); + return; + } + + fs.readFile(path.join(Config.working_folder, id + '.json'), 'utf-8', (err, data) => { + if (err) { + res.status(404); + res.end(); + return; + } + + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + }); + res.write(data); + res.end(); + }); +}); + +app.get(mapPath(`/list`), (req, res) => { + const index = readIndex(); + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + }); + res.write(JSON.stringify(index, null, 2)); + res.end(); +}); + +app.post(mapPath(`/set`), (req, res) => { + console.log('SET', req.query.name, req.query.description); + const index = readIndex(); + validateIndex(index); + + const name = (req.query.name as string || new Date().toUTCString()).substr(0, 50); + const description = (req.query.description as string || '').substr(0, 100); + + index.push({ timestamp: +new Date(), id: createv4(), name, description }); + const entry = index[index.length - 1]; + + const data = JSON.stringify({ + id: entry.id, + name, + description, + data: req.body + }); + + fs.writeFile(path.join(Config.working_folder, entry.id + '.json'), data, { encoding: 'utf8' }, () => res.end()); + writeIndex(index); +}); + +app.get(`*`, (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/plain; charset=utf-8' + }); + res.write(` +GET /list +GET /get/:id +GET /remove/:id +GET /latest +POST /set?name=...&description=... [JSON data] +`); + res.end(); +}) + +createIndex(); +app.listen(Config.port); + +console.log(`Mol* PluginState Server`); +console.log(''); +console.log(JSON.stringify(Config, null, 2)); \ No newline at end of file -- GitLab