web-api.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. /**
  2. * Copyright (c) 2018 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. */
  8. import * as express from 'express'
  9. import * as Api from './api'
  10. import * as Data from './query/data-model'
  11. import * as Coords from './algebra/coordinate'
  12. import Docs from './documentation'
  13. import ServerConfig from '../server-config'
  14. import { ConsoleLogger } from 'mol-util/console-logger'
  15. import { State } from './state'
  16. export default function init(app: express.Express) {
  17. function makePath(p: string) {
  18. return ServerConfig.apiPrefix + '/' + p;
  19. }
  20. // Header
  21. app.get(makePath(':source/:id/?$'), (req, res) => getHeader(req, res));
  22. // Box /:src/:id/box/:a1,:a2,:a3/:b1,:b2,:b3?text=0|1&space=cartesian|fractional
  23. app.get(makePath(':source/:id/box/:a1,:a2,:a3/:b1,:b2,:b3/?'), (req, res) => queryBox(req, res, getQueryParams(req, false)));
  24. // Cell /:src/:id/cell/?text=0|1&space=cartesian|fractional
  25. app.get(makePath(':source/:id/cell/?'), (req, res) => queryBox(req, res, getQueryParams(req, true)));
  26. app.get('*', (req, res) => {
  27. res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  28. res.end(Docs);
  29. });
  30. }
  31. function mapFile(type: string, id: string) {
  32. return ServerConfig.mapFile(type || '', id || '');
  33. }
  34. function wrapResponse(fn: string, res: express.Response) {
  35. const w = {
  36. do404(this: any) {
  37. if (!this.headerWritten) {
  38. res.writeHead(404);
  39. this.headerWritten = true;
  40. }
  41. this.end();
  42. },
  43. writeHeader(this: any, binary: boolean) {
  44. if (this.headerWritten) return;
  45. res.writeHead(200, {
  46. 'Content-Type': binary ? 'application/octet-stream' : 'text/plain; charset=utf-8',
  47. 'Access-Control-Allow-Origin': '*',
  48. 'Access-Control-Allow-Headers': 'X-Requested-With',
  49. 'Content-Disposition': `inline; filename="${fn}"`
  50. });
  51. this.headerWritten = true;
  52. },
  53. writeBinary(this: any, data: Uint8Array) {
  54. if (!this.headerWritten) this.writeHeader(true);
  55. return res.write(Buffer.from(data.buffer));
  56. },
  57. writeString(this: any, data: string) {
  58. if (!this.headerWritten) this.writeHeader(false);
  59. return res.write(data);
  60. },
  61. end(this: any) {
  62. if (this.ended) return;
  63. res.end();
  64. this.ended = true;
  65. },
  66. ended: false,
  67. headerWritten: false
  68. };
  69. return w;
  70. }
  71. function getSourceInfo(req: express.Request) {
  72. return {
  73. filename: mapFile(req.params.source, req.params.id),
  74. id: `${req.params.source}/${req.params.id}`
  75. };
  76. }
  77. function validateSourndAndId(req: express.Request, res: express.Response) {
  78. if (!req.params.source || req.params.source.length > 32 || !req.params.id || req.params.source.id > 32) {
  79. res.writeHead(404);
  80. res.end();
  81. ConsoleLogger.error(`Query Box`, 'Invalid source and/or id');
  82. return true;
  83. }
  84. return false;
  85. }
  86. async function getHeader(req: express.Request, res: express.Response) {
  87. if (validateSourndAndId(req, res)) {
  88. return;
  89. }
  90. let headerWritten = false;
  91. try {
  92. const { filename, id } = getSourceInfo(req);
  93. const header = await Api.getHeaderJson(filename, id);
  94. if (!header) {
  95. res.writeHead(404);
  96. return;
  97. }
  98. res.writeHead(200, {
  99. 'Content-Type': 'application/json; charset=utf-8',
  100. 'Access-Control-Allow-Origin': '*',
  101. 'Access-Control-Allow-Headers': 'X-Requested-With'
  102. });
  103. headerWritten = true;
  104. res.write(header);
  105. } catch (e) {
  106. ConsoleLogger.error(`Header ${req.params.source}/${req.params.id}`, e);
  107. if (!headerWritten) {
  108. res.writeHead(404);
  109. }
  110. } finally {
  111. res.end();
  112. }
  113. }
  114. function getQueryParams(req: express.Request, isCell: boolean): Data.QueryParams {
  115. const a = [+req.params.a1, +req.params.a2, +req.params.a3];
  116. const b = [+req.params.b1, +req.params.b2, +req.params.b3];
  117. const detail = Math.min(Math.max(0, (+req.query.detail) | 0), ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1)
  118. const isCartesian = (req.query.space || '').toLowerCase() !== 'fractional';
  119. const box: Data.QueryParamsBox = isCell
  120. ? { kind: 'Cell' }
  121. : (isCartesian
  122. ? { kind: 'Cartesian', a: Coords.cartesian(a[0], a[1], a[2]), b: Coords.cartesian(b[0], b[1], b[2]) }
  123. : { kind: 'Fractional', a: Coords.fractional(a[0], a[1], a[2]), b: Coords.fractional(b[0], b[1], b[2]) });
  124. const asBinary = (req.query.encoding || '').toLowerCase() !== 'cif';
  125. const sourceFilename = mapFile(req.params.source, req.params.id)!;
  126. return {
  127. sourceFilename,
  128. sourceId: `${req.params.source}/${req.params.id}`,
  129. asBinary,
  130. box,
  131. detail
  132. };
  133. }
  134. async function queryBox(req: express.Request, res: express.Response, params: Data.QueryParams) {
  135. if (validateSourndAndId(req, res)) {
  136. return;
  137. }
  138. const outputFilename = Api.getOutputFilename(req.params.source, req.params.id, params);
  139. const response = wrapResponse(outputFilename, res);
  140. try {
  141. if (!params.sourceFilename) {
  142. response.do404();
  143. return;
  144. }
  145. let ok = await Api.queryBox(params, () => response);
  146. if (!ok) {
  147. response.do404();
  148. return;
  149. }
  150. } catch (e) {
  151. ConsoleLogger.error(`Query Box ${JSON.stringify(req.params || {})} | ${JSON.stringify(req.query || {})}`, e);
  152. response.do404();
  153. } finally {
  154. response.end();
  155. queryDone();
  156. }
  157. }
  158. function queryDone() {
  159. if (State.shutdownOnZeroPending) {
  160. process.exit(0);
  161. }
  162. }