diff --git a/src/servers/model/local.ts b/src/servers/model/local.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..089706895d8ac9ea0b4e02b171ee9908f8b73882 100644 --- a/src/servers/model/local.ts +++ b/src/servers/model/local.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as fs from 'fs' +import Version from './version'; +import { LocalInput, runLocal } from './server/api-local'; + +console.log(`Mol* ModelServer (${Version}), (c) 2018 Mol* authors`); +console.log(``); + +let exampleWorkload: LocalInput = [{ + input: 'c:/test/quick/1tqn.cif', + output: 'c:/test/quick/localapi/1tqn_full.cif', + query: 'full', // same as defined in Api/Queries + }, { + input: 'c:/test/quick/1tqn.cif', + output: 'c:/test/quick/localapi/1tqn_full.bcif', + query: 'full', + params: { binary: true } + }, { + input: 'c:/test/quick/1cbs_updated.cif', + output: 'c:/test/quick/localapi/1cbs_ligint.cif', + query: 'residueInteraction', // action is case sensitive + params: { label_comp_id: 'REA' } + }, { + input: 'c:/test/quick/1cbs_updated.cif', // multiple files that are repeated will only be parsed once + output: 'c:/test/quick/localapi/1cbs_ligint.bcif', + query: 'residueInteraction', + params: { label_comp_id: 'REA', binary: true } // parameters are just a JSON version of the query string + } +]; + + +if (process.argv.length !== 3) { + let help = [ + `Usage: `, + ``, + ` node local jobs.json`, + ``, + `jobs.json is a JSON version of the WebAPI. Query names are case sensitive.`, + `The jobs are automatically sorted by inputFilenama and the given file is only loaded once.`, + `All processing errors are sent to stderr.`, + ``, + `Jobs example:`, + ``, + JSON.stringify(exampleWorkload, null, 2) + ]; + + console.log(help.join('\n')); +} else { + try { + const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); + runLocal(input); + } catch (e) { + console.error(e); + } +} + +// TODO: write utility that splits jobs into multiple chunks? diff --git a/src/servers/model/server.ts b/src/servers/model/server.ts index b8f9f2cb85702c09d4c2a3792e9ffe8aaa7b1e90..22e5d8db9246b5121c7e286a009457a1eaecf019 100644 --- a/src/servers/model/server.ts +++ b/src/servers/model/server.ts @@ -9,7 +9,7 @@ import * as compression from 'compression' import ServerConfig from './config' import { ConsoleLogger } from 'mol-util/console-logger'; import { PerformanceMonitor } from 'mol-util/performance-monitor'; -import { initWebApi } from './server/web-api'; +import { initWebApi } from './server/api-web'; import Version from './version' function setupShutdown() { diff --git a/src/servers/model/server/api-local.ts b/src/servers/model/server/api-local.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec7ceec0389d4415bf0905bf7134718d11b744e8 --- /dev/null +++ b/src/servers/model/server/api-local.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { JobManager, Job } from './jobs'; +import { ConsoleLogger } from 'mol-util/console-logger'; +import { resolveJob } from './query'; +import { StructureCache } from './structure-wrapper'; +import { now } from 'mol-task'; +import { PerformanceMonitor } from 'mol-util/performance-monitor'; + +export type LocalInput = { + input: string, + output: string, + query: string, + params?: any +}[]; + +export async function runLocal(input: LocalInput) { + if (!input.length) { + ConsoleLogger.error('Local', 'No input'); + return; + } + + for (const job of input) { + JobManager.add('_local_', job.input, job.query, job.params || { }, job.output); + } + JobManager.sort(); + + const started = now(); + + let job: Job | undefined = JobManager.getNext(); + let key = job.key; + let progress = 0; + while (job) { + try { + const encoder = await resolveJob(job); + const writer = wrapFile(job.outputFilename!); + encoder.writeTo(writer); + writer.end(); + ConsoleLogger.logId(job.id, 'Query', 'Written.'); + + if (JobManager.hasNext()) { + job = JobManager.getNext(); + if (key !== job.key) StructureCache.expire(key); + key = job.key; + } else { + break; + } + } catch (e) { + ConsoleLogger.errorId(job.id, e); + } + ConsoleLogger.log('Progress', `[${++progress}/${input.length}] after ${PerformanceMonitor.format(now() - started)}.`); + } + + ConsoleLogger.log('Progress', `Done in ${PerformanceMonitor.format(now() - started)}.`); + StructureCache.expireAll(); +} + +function wrapFile(fn: string) { + const w = { + open(this: any) { + if (this.opened) return; + makeDir(path.dirname(fn)); + this.file = fs.openSync(fn, 'w'); + this.opened = true; + }, + writeBinary(this: any, data: Uint8Array) { + this.open(); + fs.writeSync(this.file, new Buffer(data)); + return true; + }, + writeString(this: any, data: string) { + this.open(); + fs.writeSync(this.file, data); + return true; + }, + end(this: any) { + if (!this.opened || this.ended) return; + fs.close(this.file, function () { }); + this.ended = true; + }, + file: 0, + ended: false, + opened: false + }; + + return w; +} + +function makeDir(path: string, root?: string): boolean { + let dirs = path.split(/\/|\\/g), + dir = dirs.shift(); + + root = (root || '') + dir + '/'; + + try { fs.mkdirSync(root); } + catch (e) { + if (!fs.statSync(root).isDirectory()) throw new Error(e); + } + + return !dirs.length || makeDir(dirs.join('/'), root); +} \ No newline at end of file diff --git a/src/servers/model/server/web-api.ts b/src/servers/model/server/api-web.ts similarity index 100% rename from src/servers/model/server/web-api.ts rename to src/servers/model/server/api-web.ts diff --git a/src/servers/model/server/jobs.ts b/src/servers/model/server/jobs.ts index 3282838633900c6ceadad5b040192747347785d8..8791d26b101074bf32ded4a5e837eff91e954af1 100644 --- a/src/servers/model/server/jobs.ts +++ b/src/servers/model/server/jobs.ts @@ -22,10 +22,12 @@ export interface Job { queryDefinition: QueryDefinition, normalizedParams: any, - responseFormat: ResponseFormat + responseFormat: ResponseFormat, + + outputFilename?: string } -export function createJob(sourceId: '_local_' | string, entryId: string, queryName: string, params: any): Job { +export function createJob(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, outputFilename?: string): Job { const queryDefinition = getQueryByName(queryName); if (!queryDefinition) throw new Error(`Query '${queryName}' is not supported.`); @@ -39,7 +41,8 @@ export function createJob(sourceId: '_local_' | string, entryId: string, queryNa entryId, queryDefinition, normalizedParams, - responseFormat: { isBinary: !!params.binary } + responseFormat: { isBinary: !!params.binary }, + outputFilename }; } @@ -50,8 +53,8 @@ class _JobQueue { return this.list.count; } - add(sourceId: '_local_' | string, entryId: string, queryName: string, params: any) { - const job = createJob(sourceId, entryId, queryName, params); + add(sourceId: '_local_' | string, entryId: string, queryName: string, params: any, outputFilename?: string) { + const job = createJob(sourceId, entryId, queryName, params, outputFilename); this.list.addLast(job); return job.id; }