api-web.ts 7.2 KB

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