api-web.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import * as fs from 'fs';
  7. import * as path from 'path';
  8. import * as express from 'express';
  9. import * as bodyParser from 'body-parser'
  10. import { ModelServerConfig as Config, ModelServerConfig, mapSourceAndIdToFilename } from '../config';
  11. import { ConsoleLogger } from '../../../mol-util/console-logger';
  12. import { resolveJob } from './query';
  13. import { JobManager } from './jobs';
  14. import { UUID } from '../../../mol-util';
  15. import { QueryDefinition, normalizeRestQueryParams, normalizeRestCommonParams, QueryList } from './api';
  16. import { getApiSchema, shortcutIconLink } from './api-schema';
  17. import { swaggerUiAssetsHandler, swaggerUiIndexHandler } from '../../common/swagger-ui';
  18. function makePath(p: string) {
  19. return Config.appPrefix + '/' + p;
  20. }
  21. function wrapResponse(fn: string, res: express.Response) {
  22. const w = {
  23. doError(this: any, code = 404, message = 'Not Found.') {
  24. if (!this.headerWritten) {
  25. res.status(code).send(message);
  26. this.headerWritten = true;
  27. }
  28. this.end();
  29. },
  30. writeHeader(this: any, binary: boolean) {
  31. if (this.headerWritten) return;
  32. res.writeHead(200, {
  33. 'Content-Type': binary ? 'application/octet-stream' : 'text/plain; charset=utf-8',
  34. 'Access-Control-Allow-Origin': '*',
  35. 'Access-Control-Allow-Headers': 'X-Requested-With',
  36. 'Content-Disposition': `inline; filename="${fn}"`
  37. });
  38. this.headerWritten = true;
  39. },
  40. writeBinary(this: any, data: Uint8Array) {
  41. if (!this.headerWritten) this.writeHeader(true);
  42. return res.write(Buffer.from(data.buffer));
  43. },
  44. writeString(this: any, data: string) {
  45. if (!this.headerWritten) this.writeHeader(false);
  46. return res.write(data);
  47. },
  48. end(this: any) {
  49. if (this.ended) return;
  50. res.end();
  51. this.ended = true;
  52. },
  53. ended: false,
  54. headerWritten: false
  55. };
  56. return w;
  57. }
  58. const responseMap = new Map<UUID, express.Response>();
  59. async function processNextJob() {
  60. if (!JobManager.hasNext()) return;
  61. const job = JobManager.getNext();
  62. const response = responseMap.get(job.id)!;
  63. responseMap.delete(job.id);
  64. const filenameBase = `${job.entryId}_${job.queryDefinition.name.replace(/\s/g, '_')}`
  65. const writer = wrapResponse(job.responseFormat.isBinary ? `${filenameBase}.bcif` : `${filenameBase}.cif`, response);
  66. try {
  67. const encoder = await resolveJob(job);
  68. writer.writeHeader(job.responseFormat.isBinary);
  69. encoder.writeTo(writer);
  70. } catch (e) {
  71. ConsoleLogger.errorId(job.id, '' + e);
  72. writer.doError(404, '' + e);
  73. } finally {
  74. writer.end();
  75. ConsoleLogger.logId(job.id, 'Query', 'Finished.');
  76. setImmediate(processNextJob);
  77. }
  78. }
  79. function mapQuery(app: express.Express, queryName: string, queryDefinition: QueryDefinition) {
  80. app.get(makePath('v1/:id/' + queryName), (req, res) => {
  81. // console.log({ queryName, params: req.params, query: req.query });
  82. const entryId = req.params.id;
  83. const queryParams = normalizeRestQueryParams(queryDefinition, req.query);
  84. const commonParams = normalizeRestCommonParams(req.query);
  85. const jobId = JobManager.add({
  86. sourceId: commonParams.data_source || ModelServerConfig.defaultSource,
  87. entryId,
  88. queryName: queryName as any,
  89. queryParams,
  90. options: { modelNums: commonParams.model_nums, binary: commonParams.encoding === 'bcif' }
  91. });
  92. responseMap.set(jobId, res);
  93. if (JobManager.size === 1) processNextJob();
  94. });
  95. app.post(makePath('v1/:id/' + queryName), (req, res) => {
  96. const entryId = req.params.id;
  97. const queryParams = req.body;
  98. const commonParams = normalizeRestCommonParams(req.query);
  99. const jobId = JobManager.add({
  100. sourceId: commonParams.data_source || ModelServerConfig.defaultSource,
  101. entryId,
  102. queryName: queryName as any,
  103. queryParams,
  104. options: { modelNums: commonParams.model_nums, binary: commonParams.encoding === 'bcif' }
  105. });
  106. responseMap.set(jobId, res);
  107. if (JobManager.size === 1) processNextJob();
  108. });
  109. }
  110. export function initWebApi(app: express.Express) {
  111. app.use(bodyParser.json({ limit: '1mb' }));
  112. app.get(makePath('static/:format/:id'), async (req, res) => {
  113. const binary = req.params.format === 'bcif';
  114. const id = req.params.id;
  115. const fn = mapSourceAndIdToFilename(binary ? 'pdb-bcif' : 'pdb-cif', id);
  116. if (!fn || !fs.existsSync(fn)) {
  117. res.status(404);
  118. res.end();
  119. return;
  120. }
  121. fs.readFile(fn, (err, data) => {
  122. if (err) {
  123. res.status(404);
  124. res.end();
  125. return;
  126. }
  127. const f = path.parse(fn);
  128. res.writeHead(200, {
  129. 'Content-Type': binary ? 'application/octet-stream' : 'text/plain; charset=utf-8',
  130. 'Access-Control-Allow-Origin': '*',
  131. 'Access-Control-Allow-Headers': 'X-Requested-With',
  132. 'Content-Disposition': `inline; filename="${f.name}${f.ext}"`
  133. });
  134. res.write(data);
  135. res.end();
  136. });
  137. })
  138. // app.get(makePath('v1/json'), (req, res) => {
  139. // const query = /\?(.*)$/.exec(req.url)![1];
  140. // const args = JSON.parse(decodeURIComponent(query));
  141. // const name = args.name;
  142. // const entryId = args.id;
  143. // const queryParams = args.params || { };
  144. // const jobId = JobManager.add({
  145. // sourceId: 'pdb',
  146. // entryId,
  147. // queryName: name,
  148. // queryParams,
  149. // options: { modelNums: args.modelNums, binary: args.binary }
  150. // });
  151. // responseMap.set(jobId, res);
  152. // if (JobManager.size === 1) processNextJob();
  153. // });
  154. app.use(bodyParser.json({ limit: '20mb' }));
  155. for (const q of QueryList) {
  156. mapQuery(app, q.name, q.definition);
  157. }
  158. const schema = getApiSchema();
  159. app.get(makePath('openapi.json'), (req, res) => {
  160. res.writeHead(200, {
  161. 'Content-Type': 'application/json; charset=utf-8',
  162. 'Access-Control-Allow-Origin': '*',
  163. 'Access-Control-Allow-Headers': 'X-Requested-With'
  164. });
  165. res.end(JSON.stringify(schema));
  166. });
  167. app.use(makePath(''), swaggerUiAssetsHandler());
  168. app.get(makePath(''), swaggerUiIndexHandler({
  169. openapiJsonUrl: makePath('openapi.json'),
  170. apiPrefix: Config.appPrefix,
  171. title: 'ModelServer API',
  172. shortcutIconLink
  173. }));
  174. // app.get('*', (req, res) => {
  175. // res.send(LandingPage);
  176. // });
  177. }