web-api.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
  5. *
  6. * @author David Sehnal <david.sehnal@gmail.com>
  7. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  8. */
  9. import * as express from 'express';
  10. import * as Api from './api';
  11. import * as Data from './query/data-model';
  12. import * as Coords from './algebra/coordinate';
  13. import { ConsoleLogger } from '../../../mol-util/console-logger';
  14. import { State } from './state';
  15. import { LimitsConfig, ServerConfig } from '../config';
  16. import { interpolate } from '../../../mol-util/string';
  17. import { getSchema, shortcutIconLink } from './web-schema';
  18. import { swaggerUiIndexHandler, swaggerUiAssetsHandler } from '../../common/swagger-ui';
  19. export default function init(app: express.Express) {
  20. app.locals.mapFile = getMapFileFn();
  21. function makePath(p: string) {
  22. return `${ServerConfig.apiPrefix}/${p}`;
  23. }
  24. // Header
  25. app.get(makePath(':source/:id/?$'), (req, res) => getHeader(req, res));
  26. // Box /:src/:id/box/:a1,:a2,:a3/:b1,:b2,:b3?text=0|1&space=cartesian|fractional
  27. app.get(makePath(':source/:id/box/:a1,:a2,:a3/:b1,:b2,:b3/?'), (req, res) => queryBox(req, res, getQueryParams(req, false)));
  28. // Cell /:src/:id/cell/?text=0|1&space=cartesian|fractional
  29. app.get(makePath(':source/:id/cell/?'), (req, res) => queryBox(req, res, getQueryParams(req, true)));
  30. app.get(makePath('openapi.json'), (req, res) => {
  31. res.writeHead(200, {
  32. 'Content-Type': 'application/json; charset=utf-8',
  33. 'Access-Control-Allow-Origin': '*',
  34. 'Access-Control-Allow-Headers': 'X-Requested-With'
  35. });
  36. res.end(JSON.stringify(getSchema()));
  37. });
  38. app.use(makePath(''), swaggerUiAssetsHandler());
  39. app.get(makePath(''), swaggerUiIndexHandler({
  40. openapiJsonUrl: makePath('openapi.json'),
  41. apiPrefix: ServerConfig.apiPrefix,
  42. title: 'VolumeServer API',
  43. shortcutIconLink
  44. }));
  45. }
  46. function getMapFileFn() {
  47. const map = new Function('type', 'id', 'interpolate', [
  48. 'id = id.toLowerCase()',
  49. 'switch (type.toLowerCase()) {',
  50. ...ServerConfig.idMap.map(mapping => {
  51. const [type, path] = mapping;
  52. return ` case '${type}': return interpolate('${path}', { id });`;
  53. }),
  54. ' default: return void 0;',
  55. '}'
  56. ].join('\n'));
  57. return (type: string, id: string) => map(type, id, interpolate);
  58. }
  59. function wrapResponse(fn: string, res: express.Response) {
  60. return {
  61. do404(this: any) {
  62. if (!this.headerWritten) {
  63. res.writeHead(404);
  64. this.headerWritten = true;
  65. }
  66. this.end();
  67. },
  68. writeHeader(this: any, binary: boolean) {
  69. if (this.headerWritten) return;
  70. res.writeHead(200, {
  71. 'Content-Type': binary ? 'application/octet-stream' : 'text/plain; charset=utf-8',
  72. 'Access-Control-Allow-Origin': '*',
  73. 'Access-Control-Allow-Headers': 'X-Requested-With',
  74. 'Content-Disposition': `inline; filename="${fn}"`
  75. });
  76. this.headerWritten = true;
  77. },
  78. writeBinary(this: any, data: Uint8Array) {
  79. if (!this.headerWritten) this.writeHeader(true);
  80. return res.write(Buffer.from(data.buffer));
  81. },
  82. writeString(this: any, data: string) {
  83. if (!this.headerWritten) this.writeHeader(false);
  84. return res.write(data);
  85. },
  86. end(this: any) {
  87. if (this.ended) return;
  88. res.end();
  89. this.ended = true;
  90. },
  91. ended: false,
  92. headerWritten: false
  93. };
  94. }
  95. function getSourceInfo(req: express.Request) {
  96. return {
  97. filename: req.app.locals.mapFile(req.params.source, req.params.id),
  98. id: `${req.params.source}/${req.params.id}`
  99. };
  100. }
  101. function validateSourceAndId(req: express.Request, res: express.Response) {
  102. if (!req.params.source || req.params.source.length > 32 || !req.params.id || req.params.id.length > 32) {
  103. res.writeHead(404);
  104. res.end();
  105. ConsoleLogger.error(`Query Box`, 'Invalid source and/or id');
  106. return true;
  107. }
  108. return false;
  109. }
  110. async function getHeader(req: express.Request, res: express.Response) {
  111. if (validateSourceAndId(req, res)) {
  112. return;
  113. }
  114. let headerWritten = false;
  115. try {
  116. const { filename, id } = getSourceInfo(req);
  117. const header = await Api.getExtendedHeaderJson(filename, id);
  118. if (!header) {
  119. res.writeHead(404);
  120. return;
  121. }
  122. res.writeHead(200, {
  123. 'Content-Type': 'application/json; charset=utf-8',
  124. 'Access-Control-Allow-Origin': '*',
  125. 'Access-Control-Allow-Headers': 'X-Requested-With'
  126. });
  127. headerWritten = true;
  128. res.write(header);
  129. } catch (e) {
  130. ConsoleLogger.error(`Header ${req.params.source}/${req.params.id}`, e);
  131. if (!headerWritten) {
  132. res.writeHead(404);
  133. }
  134. } finally {
  135. res.end();
  136. }
  137. }
  138. function getQueryParams(req: express.Request, isCell: boolean): Data.QueryParams {
  139. const a = [+req.params.a1, +req.params.a2, +req.params.a3];
  140. const b = [+req.params.b1, +req.params.b2, +req.params.b3];
  141. const detail = Math.min(Math.max(0, (+req.query.detail!) | 0), LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1);
  142. const isCartesian = (req.query.space as string || '').toLowerCase() !== 'fractional';
  143. const box: Data.QueryParamsBox = isCell
  144. ? { kind: 'Cell' }
  145. : (isCartesian
  146. ? { kind: 'Cartesian', a: Coords.cartesian(a[0], a[1], a[2]), b: Coords.cartesian(b[0], b[1], b[2]) }
  147. : { kind: 'Fractional', a: Coords.fractional(a[0], a[1], a[2]), b: Coords.fractional(b[0], b[1], b[2]) });
  148. const asBinary = (req.query.encoding as string || '').toLowerCase() !== 'cif';
  149. const sourceFilename = req.app.locals.mapFile(req.params.source, req.params.id)!;
  150. return {
  151. sourceFilename,
  152. sourceId: `${req.params.source}/${req.params.id}`,
  153. asBinary,
  154. box,
  155. detail
  156. };
  157. }
  158. async function queryBox(req: express.Request, res: express.Response, params: Data.QueryParams) {
  159. if (validateSourceAndId(req, res)) {
  160. return;
  161. }
  162. const outputFilename = Api.getOutputFilename(req.params.source, req.params.id, params);
  163. const response = wrapResponse(outputFilename, res);
  164. try {
  165. if (!params.sourceFilename) {
  166. response.do404();
  167. return;
  168. }
  169. let ok = await Api.queryBox(params, () => response);
  170. if (!ok) {
  171. response.do404();
  172. return;
  173. }
  174. } catch (e) {
  175. ConsoleLogger.error(`Query Box ${JSON.stringify(req.params || {})} | ${JSON.stringify(req.query || {})}`, e);
  176. response.do404();
  177. } finally {
  178. response.end();
  179. queryDone();
  180. }
  181. }
  182. function queryDone() {
  183. if (State.shutdownOnZeroPending) {
  184. process.exit(0);
  185. }
  186. }