瀏覽代碼

plugin-state server swagger ui

David Sehnal 5 年之前
父節點
當前提交
29e47c1e90

+ 2 - 1
src/mol-plugin/behavior/static/state.ts

@@ -14,6 +14,7 @@ import { getFormattedTime } from '../../../mol-util/date';
 import { readFromFile } from '../../../mol-util/data-source';
 import { download } from '../../../mol-util/download';
 import { Structure } from '../../../mol-model/structure';
+import { urlCombine } from '../../../mol-util/url';
 
 export function registerDefault(ctx: PluginContext) {
     SyncBehaviors(ctx);
@@ -157,7 +158,7 @@ export function Snapshots(ctx: PluginContext) {
     });
 
     PluginCommands.State.Snapshots.Upload.subscribe(ctx, ({ name, description, playOnLoad, serverUrl }) => {
-        return fetch(`${serverUrl}/set?name=${encodeURIComponent(name || '')}&description=${encodeURIComponent(description || '')}`, {
+        return fetch(urlCombine(serverUrl, `set?name=${encodeURIComponent(name || '')}&description=${encodeURIComponent(description || '')}`), {
             method: 'POST',
             mode: 'cors',
             referrer: 'no-referrer',

+ 134 - 0
src/servers/plugin-state/api-schema.ts

@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import VERSION from './version'
+import { Config } from './config';
+
+export function getSchema(config: Config) {
+    function mapPath(path: string) {
+        return `${config.api_prefix}/${path}`;
+    }
+
+    return {
+        openapi: '3.0.0',
+        info: {
+            version: VERSION,
+            title: 'PluginState Server',
+            description: 'The PluginState Server is a simple service for storing and retreiving states of the Mol* Viewer app.',
+        },
+        tags: [
+            {
+                name: 'General',
+            }
+        ],
+        paths: {
+            [mapPath(`list/`)]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Returns a JSON response with the list of currently stored states.',
+                    operationId: 'list',
+                    parameters: [],
+                    responses: {
+                        200: {
+                            description: 'A list of stored states',
+                            content: {
+                                'application/json': { }
+                            }
+                        }
+                    },
+                }
+            },
+            [mapPath(`get/{id}`)]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Returns the Mol* Viewer state with the given id.',
+                    operationId: 'get',
+                    parameters: [
+                        {
+                            name: 'id',
+                            in: 'path',
+                            description: `Id of the state.`,
+                            required: true,
+                            schema: { type: 'string' },
+                            style: 'simple'
+                        }
+                    ],
+                    responses: {
+                        200: {
+                            description: 'A JSON object with the state.',
+                            content: {
+                                'application/json': { }
+                            }
+                        }
+                    },
+                }
+            },
+            [mapPath(`remove/{id}`)]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Removes the Mol* Viewer state with the given id.',
+                    operationId: 'remove',
+                    parameters: [
+                        {
+                            name: 'id',
+                            in: 'path',
+                            description: `Id of the state.`,
+                            required: true,
+                            schema: { type: 'string' },
+                            style: 'simple'
+                        }
+                    ],
+                    responses: {
+                        200: {
+                            description: 'Empty response.',
+                            content: { 'text/plain': { } }
+                        }
+                    },
+                }
+            },
+            [mapPath(`set/`)]: {
+                post: {
+                    tags: ['General'],
+                    summary: `Post Mol* Viewer state to the server. At most ${config.max_states} states can be stored. If the limit is reached, older states will be removed.`,
+                    operationId: 'set',
+                    requestBody: {
+                        content: {
+                            'application/json': {
+                                schema: { type: 'object' }
+                            }
+                        }
+                    },
+                    parameters: [
+                        {
+                            name: 'name',
+                            in: 'query',
+                            description: `Name of the state. If none provided, current UTC date-time is used.`,
+                            required: false,
+                            schema: { type: 'string' },
+                            style: 'simple'
+                        },
+                        {
+                            name: 'description',
+                            in: 'query',
+                            description: `Description of the state.`,
+                            required: false,
+                            schema: { type: 'string' },
+                            style: 'simple'
+                        }
+                    ],
+                    responses: {
+                        200: {
+                            description: 'Empty response.',
+                            content: { 'text/plain': { } }
+                        }
+                    },
+                }
+            },
+        }
+    }
+}
+
+export const shortcutIconLink = `<link rel='shortcut icon' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAnUExURQAAAMIrHrspHr0oH7soILonHrwqH7onILsoHrsoH7soH7woILwpIKgVokoAAAAMdFJOUwAQHzNxWmBHS5XO6jdtAmoAAACZSURBVDjLxZNRCsQgDAVNXmwb9f7nXZEaLRgXloXOhwQdjMYYwpOLw55fBT46KhbOKhmRR2zLcFJQj8UR+HxFgArIF5BKJbEncC6NDEdI5SatBRSDJwGAoiFDONrEJXWYhGMIcRJGCrb1TOtDahfUuQXd10jkFYq0ViIrbUpNcVT6redeC1+b9tH2WLR93Sx2VCzkv/7NjfABxjQHksGB7lAAAAAASUVORK5CYII=' />`

+ 28 - 0
src/servers/plugin-state/config.ts

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as argparse from 'argparse'
+
+export interface Config {
+    working_folder: string,
+    port?: string | number,
+    api_prefix: string,
+    max_states: number
+}
+
+export function getConfig() {
+    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(['--api-prefix'], { help: 'Server API 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;
+    return config;
+}

+ 49 - 68
src/servers/plugin-state/index.ts

@@ -8,29 +8,15 @@ 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 { swaggerUiIndexHandler, swaggerUiAssetsHandler } from '../common/swagger-ui';
 import { makeDir } from '../../mol-util/make-dir'
+import { getConfig } from './config'
+import { UUID } from '../../mol-util'
+import { shortcutIconLink, getSchema } from './api-schema'
 
-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 Config = getConfig();
 
 const app = express();
 app.use(compression(<any>{ level: 6, memLevel: 9, chunkSize: 16 * 16384, filter: () => true }));
@@ -70,7 +56,7 @@ function validateIndex(index: Index) {
                 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'))
@@ -113,19 +99,9 @@ function clear() {
     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}`;
+    if (!Config.api_prefix) return path;
+    return `/${Config.api_prefix}/${path}`;
 }
 
 app.get(mapPath(`/get/:id`), (req, res) => {
@@ -164,30 +140,30 @@ app.get(mapPath(`/remove/:id`), (req, res) => {
     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(`/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();
@@ -206,7 +182,7 @@ app.post(mapPath(`/set`), (req, res) => {
     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 });
+    index.push({ timestamp: +new Date(), id: UUID.createv4(), name, description });
     const entry = index[index.length - 1];
 
     const data = JSON.stringify({
@@ -220,19 +196,24 @@ app.post(mapPath(`/set`), (req, res) => {
     writeIndex(index);
 });
 
-app.get(`*`, (req, res) => {
+const schema = getSchema(Config);
+app.get(mapPath('/openapi.json'), (req, res) => {
     res.writeHead(200, {
-        'Content-Type': 'text/plain; charset=utf-8'
+        'Content-Type': 'application/json; charset=utf-8',
+        'Access-Control-Allow-Origin': '*',
+        'Access-Control-Allow-Headers': 'X-Requested-With'
     });
-    res.write(`
-GET /list
-GET /get/:id
-GET /remove/:id
-GET /latest
-POST /set?name=...&description=... [JSON data]
-`);
-    res.end();
-})
+    res.end(JSON.stringify(schema));
+});
+
+app.use(mapPath('/'), swaggerUiAssetsHandler());
+app.get(mapPath('/'), swaggerUiIndexHandler({
+    openapiJsonUrl: mapPath('/openapi.json'),
+    apiPrefix: Config.api_prefix,
+    title: 'PluginState Server API',
+    shortcutIconLink
+}));
+
 
 createIndex();
 app.listen(Config.port);

+ 1 - 0
src/servers/plugin-state/version.ts

@@ -0,0 +1 @@
+export default '0.1.0'