index.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. /**
  2. * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import * as express from 'express'
  7. import * as compression from 'compression'
  8. import * as cors from 'cors'
  9. import * as bodyParser from 'body-parser'
  10. import * as argparse from 'argparse'
  11. import * as fs from 'fs'
  12. import * as path from 'path'
  13. import { makeDir } from '../../mol-util/make-dir'
  14. interface Config {
  15. working_folder: string,
  16. port?: string | number,
  17. app_prefix: string,
  18. max_states: number
  19. }
  20. const cmdParser = new argparse.ArgumentParser({
  21. addHelp: true
  22. });
  23. cmdParser.addArgument(['--working-folder'], { help: 'Working forlder path.', required: true });
  24. cmdParser.addArgument(['--port'], { help: 'Server port. Altenatively use ENV variable PORT.', type: 'int', required: false });
  25. cmdParser.addArgument(['--app-prefix'], { help: 'Server app prefix.', defaultValue: '', required: false });
  26. cmdParser.addArgument(['--max-states'], { help: 'Maxinum number of states that could be saved.', defaultValue: 40, type: 'int', required: false });
  27. const Config = cmdParser.parseArgs() as Config;
  28. if (!Config.port) Config.port = process.env.port || 1339;
  29. const app = express();
  30. app.use(compression(<any>{ level: 6, memLevel: 9, chunkSize: 16 * 16384, filter: () => true }));
  31. app.use(cors({ methods: ['GET', 'PUT'] }));
  32. app.use(bodyParser.json({ limit: '20mb' }));
  33. type Index = { timestamp: number, id: string, name: string, description: string, isSticky?: boolean }[]
  34. function createIndex() {
  35. const fn = path.join(Config.working_folder, 'index.json');
  36. if (fs.existsSync(fn)) return;
  37. if (!fs.existsSync(Config.working_folder)) makeDir(Config.working_folder);
  38. fs.writeFileSync(fn, '[]', 'utf-8');
  39. }
  40. function writeIndex(index: Index) {
  41. const fn = path.join(Config.working_folder, 'index.json');
  42. if (!fs.existsSync(Config.working_folder)) makeDir(Config.working_folder);
  43. fs.writeFileSync(fn, JSON.stringify(index, null, 2), 'utf-8');
  44. }
  45. function readIndex() {
  46. const fn = path.join(Config.working_folder, 'index.json');
  47. if (!fs.existsSync(fn)) return [];
  48. return JSON.parse(fs.readFileSync(fn, 'utf-8')) as Index;
  49. }
  50. function validateIndex(index: Index) {
  51. if (index.length > Config.max_states) {
  52. const deletes: Index = [], newIndex: Index = [];
  53. const toDelete = index.length - Config.max_states;
  54. for (const e of index) {
  55. if (!e.isSticky && deletes.length < toDelete) {
  56. deletes.push(e);
  57. } else {
  58. newIndex.push(e);
  59. }
  60. }
  61. // index.slice(0, index.length - 30);
  62. for (const d of deletes) {
  63. try {
  64. fs.unlinkSync(path.join(Config.working_folder, d.id + '.json'))
  65. } catch { }
  66. }
  67. return newIndex;
  68. }
  69. return index;
  70. }
  71. function remove(id: string) {
  72. let index = readIndex();
  73. let i = 0;
  74. for (const e of index) {
  75. if (e.id !== id) {
  76. i++;
  77. continue;
  78. }
  79. try {
  80. for (let j = i + 1; j < index.length; j++) {
  81. index[j - 1] = index[j];
  82. }
  83. index.pop();
  84. writeIndex(index);
  85. } catch { }
  86. try {
  87. fs.unlinkSync(path.join(Config.working_folder, e.id + '.json'))
  88. } catch { }
  89. return;
  90. }
  91. }
  92. function clear() {
  93. let index = readIndex();
  94. for (const e of index) {
  95. try {
  96. fs.unlinkSync(path.join(Config.working_folder, e.id + '.json'))
  97. } catch { }
  98. }
  99. writeIndex([]);
  100. }
  101. export function createv4() {
  102. let d = (+new Date());
  103. const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  104. const r = (d + Math.random()*16)%16 | 0;
  105. d = Math.floor(d/16);
  106. return (c==='x' ? r : (r&0x3|0x8)).toString(16);
  107. });
  108. return uuid.toLowerCase();
  109. }
  110. function mapPath(path: string) {
  111. if (!Config.app_prefix) return path;
  112. return `/${Config.app_prefix}/${path}`;
  113. }
  114. app.get(mapPath(`/get/:id`), (req, res) => {
  115. const id: string = req.params.id || '';
  116. console.log('Reading', id);
  117. if (id.length === 0 || id.indexOf('.') >= 0 || id.indexOf('/') >= 0 || id.indexOf('\\') >= 0) {
  118. res.status(404);
  119. res.end();
  120. return;
  121. }
  122. fs.readFile(path.join(Config.working_folder, id + '.json'), 'utf-8', (err, data) => {
  123. if (err) {
  124. res.status(404);
  125. res.end();
  126. return;
  127. }
  128. res.writeHead(200, {
  129. 'Content-Type': 'application/json; charset=utf-8',
  130. });
  131. res.write(data);
  132. res.end();
  133. });
  134. });
  135. app.get(mapPath(`/clear`), (req, res) => {
  136. clear();
  137. res.status(200);
  138. res.end();
  139. });
  140. app.get(mapPath(`/remove/:id`), (req, res) => {
  141. remove((req.params.id as string || '').toLowerCase());
  142. res.status(200);
  143. res.end();
  144. });
  145. app.get(mapPath(`/latest`), (req, res) => {
  146. const index = readIndex();
  147. const id: string = index.length > 0 ? index[index.length - 1].id : '';
  148. console.log('Reading', id);
  149. if (id.length === 0 || id.indexOf('.') >= 0 || id.indexOf('/') >= 0 || id.indexOf('\\') >= 0) {
  150. res.status(404);
  151. res.end();
  152. return;
  153. }
  154. fs.readFile(path.join(Config.working_folder, id + '.json'), 'utf-8', (err, data) => {
  155. if (err) {
  156. res.status(404);
  157. res.end();
  158. return;
  159. }
  160. res.writeHead(200, {
  161. 'Content-Type': 'application/json; charset=utf-8',
  162. });
  163. res.write(data);
  164. res.end();
  165. });
  166. });
  167. app.get(mapPath(`/list`), (req, res) => {
  168. const index = readIndex();
  169. res.writeHead(200, {
  170. 'Content-Type': 'application/json; charset=utf-8',
  171. });
  172. res.write(JSON.stringify(index, null, 2));
  173. res.end();
  174. });
  175. app.post(mapPath(`/set`), (req, res) => {
  176. console.log('SET', req.query.name, req.query.description);
  177. const index = readIndex();
  178. validateIndex(index);
  179. const name = (req.query.name as string || new Date().toUTCString()).substr(0, 50);
  180. const description = (req.query.description as string || '').substr(0, 100);
  181. index.push({ timestamp: +new Date(), id: createv4(), name, description });
  182. const entry = index[index.length - 1];
  183. const data = JSON.stringify({
  184. id: entry.id,
  185. name,
  186. description,
  187. data: req.body
  188. });
  189. fs.writeFile(path.join(Config.working_folder, entry.id + '.json'), data, { encoding: 'utf8' }, () => res.end());
  190. writeIndex(index);
  191. });
  192. app.get(`*`, (req, res) => {
  193. res.writeHead(200, {
  194. 'Content-Type': 'text/plain; charset=utf-8'
  195. });
  196. res.write(`
  197. GET /list
  198. GET /get/:id
  199. GET /remove/:id
  200. GET /latest
  201. POST /set?name=...&description=... [JSON data]
  202. `);
  203. res.end();
  204. })
  205. createIndex();
  206. app.listen(Config.port);
  207. console.log(`Mol* PluginState Server`);
  208. console.log('');
  209. console.log(JSON.stringify(Config, null, 2));