api-web.ts 7.0 KB

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