api-web.ts 6.9 KB

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