From 305719c58d0e5ee7f47603b4d082fcc320105b4d Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Mon, 21 Oct 2019 09:13:26 +0200 Subject: [PATCH 01/12] Register tropy protocol --- src/browser/main.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/browser/main.js b/src/browser/main.js index 2983c2f59f..6f397b6e2d 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -92,6 +92,10 @@ if (!(win32 && require('./squirrel')(opts))) { info(`ready after ${tropy.ready - START}ms [req:${T2 - T1}ms]`) }) + if (app.isPackaged) { + app.setAsDefaultProtocolClient('tropy') + } + if (darwin) { app.on('open-file', (event, file) => { if (tropy.ready) { From 2459df0a71a72727889ce5a94ad54627fa56051d Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Mon, 21 Oct 2019 09:28:46 +0200 Subject: [PATCH 02/12] Start API server if port configured --- src/browser/api.js | 21 +++++++++++++++++++++ src/browser/args.js | 7 +++++++ src/browser/tropy.js | 8 ++++++++ 3 files changed, 36 insertions(+) create mode 100644 src/browser/api.js diff --git a/src/browser/api.js b/src/browser/api.js new file mode 100644 index 0000000000..9f6cf0cb96 --- /dev/null +++ b/src/browser/api.js @@ -0,0 +1,21 @@ +'use strict' + +const Koa = require('koa') + +class Server { + + constructor(app) { + this.app = app + this.koa = new Koa + + this.koa.use(async ctx => { + ctx.body = this.app.version + }) + } + + start() { + this.koa.listen(this.app.opts.port) + } +} + +module.exports = Server diff --git a/src/browser/args.js b/src/browser/args.js index c564cf062d..08b2cc256a 100644 --- a/src/browser/args.js +++ b/src/browser/args.js @@ -72,6 +72,13 @@ module.exports = default: false }) + .option('port', { + alias: 'p', + type: 'number', + describe: 'Set API listening port', + default: null + }) + .help('help') .version(version) diff --git a/src/browser/tropy.js b/src/browser/tropy.js index 235572de53..80d7754c59 100644 --- a/src/browser/tropy.js +++ b/src/browser/tropy.js @@ -94,12 +94,20 @@ class Tropy extends EventEmitter { prop(this, 'plugins', { value: new Plugins(join(opts.data, 'plugins')) }) + + prop(this, 'api', { + value: this.opts.port && new (require('./api'))(this) + }) } async start() { await this.restore() this.listen() this.wm.start() + + if (this.opts.port) { + this.api.start() + } } stop() { From f2200f99b773edcfeaf87e0c1b02cf6a285da9bc Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Mon, 21 Oct 2019 11:03:54 +0200 Subject: [PATCH 03/12] Move api server implementation to common --- src/browser/api.js | 31 ++++++++++++++++++++++--------- src/browser/tropy.js | 12 ++++-------- src/common/api.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 src/common/api.js diff --git a/src/browser/api.js b/src/browser/api.js index 9f6cf0cb96..1fd09fa29f 100644 --- a/src/browser/api.js +++ b/src/browser/api.js @@ -1,21 +1,34 @@ 'use strict' -const Koa = require('koa') - class Server { - constructor(app) { + if (app.opts.port) { + let api = require('../common/api') + let { logger } = require('../common/log') + + this.koa = api.create({ + dispatch: this.dispatch, + log: logger.child({ name: 'api' }), + version: app.version + }) + } + this.app = app - this.koa = new Koa + } - this.koa.use(async ctx => { - ctx.body = this.app.version - }) + dispatch = async () => { } start() { - this.koa.listen(this.app.opts.port) + if (this.koa) { + this.koa.listen(this.app.opts.port) + } + } + + stop() { } } -module.exports = Server +module.exports = { + Server +} diff --git a/src/browser/tropy.js b/src/browser/tropy.js index 80d7754c59..979156a41b 100644 --- a/src/browser/tropy.js +++ b/src/browser/tropy.js @@ -32,6 +32,7 @@ const { Strings } = require('../common/res') const Storage = require('./storage') const Updater = require('./updater') const dialog = require('./dialog') +const API = require('./api') const WindowManager = require('./wm') const { addIdleObserver } = require('./idle') const { migrate } = require('./migrate') @@ -78,6 +79,7 @@ class Tropy extends EventEmitter { this.updater = new Updater({ enable: process.env.NODE_ENV === 'production' && opts['auto-updates'] }) + this.api = new API.Server(this) prop(this, 'cache', { value: new Cache(opts.cache || join(opts.data, 'cache')) @@ -94,23 +96,17 @@ class Tropy extends EventEmitter { prop(this, 'plugins', { value: new Plugins(join(opts.data, 'plugins')) }) - - prop(this, 'api', { - value: this.opts.port && new (require('./api'))(this) - }) } async start() { await this.restore() this.listen() this.wm.start() - - if (this.opts.port) { - this.api.start() - } + this.api.start() } stop() { + this.api.stop() this.updater.stop() this.plugins.stop() this.persist() diff --git a/src/common/api.js b/src/common/api.js new file mode 100644 index 0000000000..858a70441c --- /dev/null +++ b/src/common/api.js @@ -0,0 +1,30 @@ +'use strict' + +const Koa = require('koa') + +const api = { + create({ dispatch, log, version }) { + let app = new Koa + + app.silent = true + app.on('error', e => { + log.error({ stack: e.stack }, e.message) + }) + + app.context.dispatch = dispatch + app.context.log = log + app.context.version = version + + app + .use(api.version) + + return app + }, + + + async version(ctx) { + ctx.body = ctx.version + } +} + +module.exports = api From 494ad26d59ad90fbce136b20f1b88d05c77ee402 Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Mon, 21 Oct 2019 20:44:45 +0200 Subject: [PATCH 04/12] Log API requests --- scripts/log-viewer.js | 13 +++++++++---- src/browser/api.js | 7 +++++-- src/common/api.js | 17 ++++++++++++++++- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/scripts/log-viewer.js b/scripts/log-viewer.js index 8812dc606e..856225c4d7 100755 --- a/scripts/log-viewer.js +++ b/scripts/log-viewer.js @@ -17,10 +17,14 @@ const format = log => const end = log => log.quit ? '\n\n' : '\n' -const body = log => - log.action ? - `${log.action} ${chalk.gray(meta(log))}` : - log.msg || log.message +const body = log => { + if (log.action) + return `${log.action} ${chalk.gray(meta(log))}` + if (log.url) + return `${log.url} ${chalk.gray(`${log.status} Δ${ms(log.ms)}`)}` + else + return log.msg || log.message +} const error = ({ stack }) => stack ? @@ -43,6 +47,7 @@ const symbol = log => const SYMBOL = { about: 'α', + api: 'λ', main: 'β', prefs: 'σ', print: 'π', diff --git a/src/browser/api.js b/src/browser/api.js index 1fd09fa29f..ff56de2f3a 100644 --- a/src/browser/api.js +++ b/src/browser/api.js @@ -1,10 +1,11 @@ 'use strict' +const { info, logger } = require('../common/log') + class Server { constructor(app) { if (app.opts.port) { let api = require('../common/api') - let { logger } = require('../common/log') this.koa = api.create({ dispatch: this.dispatch, @@ -21,7 +22,9 @@ class Server { start() { if (this.koa) { - this.koa.listen(this.app.opts.port) + let { port } = this.app.opts + info(`starting api on port ${port}`) + this.koa.listen(port) } } diff --git a/src/common/api.js b/src/common/api.js index 858a70441c..072611c986 100644 --- a/src/common/api.js +++ b/src/common/api.js @@ -8,7 +8,10 @@ const api = { app.silent = true app.on('error', e => { - log.error({ stack: e.stack }, e.message) + log.error({ + stack: e.stack, + status: e.status + }, e.message) }) app.context.dispatch = dispatch @@ -16,12 +19,24 @@ const api = { app.context.version = version app + .use(api.logging) .use(api.version) return app }, + async logging(ctx, next) { + const START = Date.now() + await next() + + ctx.log.debug({ + ms: Date.now() - START, + status: ctx.status, + url: ctx.url + }) + }, + async version(ctx) { ctx.body = ctx.version } From 710eb8e2261693dc3ba1e6cab6e2a70a0c42d309 Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Mon, 21 Oct 2019 21:09:15 +0200 Subject: [PATCH 05/12] Use koa router --- src/common/api.js | 72 ++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/common/api.js b/src/common/api.js index 072611c986..47d0b67a5d 100644 --- a/src/common/api.js +++ b/src/common/api.js @@ -1,45 +1,47 @@ 'use strict' const Koa = require('koa') - -const api = { - create({ dispatch, log, version }) { - let app = new Koa - - app.silent = true - app.on('error', e => { - log.error({ - stack: e.stack, - status: e.status - }, e.message) +const Router = require('@koa/router') + +const create = ({ dispatch, log, version }) => { + let app = new Koa + let api = new Router + + app.silent = true + app.on('error', e => { + log.error({ + stack: e.stack, + status: e.status + }, e.message) + }) + + app.context.dispatch = dispatch + app.context.log = log + + api + .get('/version', (ctx) => { + ctx.body = { version } }) - app.context.dispatch = dispatch - app.context.log = log - app.context.version = version - - app - .use(api.logging) - .use(api.version) - - return app - }, + app + .use(logging) + .use(api.routes()) + .use(api.allowedMethods()) + return app +} - async logging(ctx, next) { - const START = Date.now() - await next() - - ctx.log.debug({ - ms: Date.now() - START, - status: ctx.status, - url: ctx.url - }) - }, +const logging = async (ctx, next) => { + const START = Date.now() + await next() - async version(ctx) { - ctx.body = ctx.version - } + ctx.log.debug({ + ms: Date.now() - START, + status: ctx.status, + url: ctx.url + }) } -module.exports = api +module.exports = { + create +} From 6712ace23903b88eb7add89375f8913bc3ec375c Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Mon, 21 Oct 2019 21:10:17 +0200 Subject: [PATCH 06/12] Install Koa --- package-lock.json | 318 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + 2 files changed, 317 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 05b3ee36f2..a4b65316ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -359,6 +359,29 @@ "resolved": "https://registry.npmjs.org/@inukshuk/exif/-/exif-2.0.0.tgz", "integrity": "sha512-fAvsxo5Fq68hu02adHpMnh+zBJqlnghVg/NCKE6CzHafxmxUClk8RXuGAuduFAZkQEi1DJiqxUJOBITGEeLq1w==" }, + "@koa/router": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-8.0.2.tgz", + "integrity": "sha512-7Wa8yXBmz9HjmZOr+xfMVuxFPNObdkiQFBiwF9SQ8zFqHykwBHcJA/mLqqxU2NKoeXRPBKUOPeOjwgR+gyadcA==", + "requires": { + "debug": "^3.1.0", + "http-errors": "^1.3.1", + "koa-compose": "^3.0.0", + "methods": "^1.0.1", + "path-to-regexp": "^1.1.1", + "urijs": "^1.19.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -582,6 +605,15 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, "accessibility-developer-tools": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz", @@ -660,6 +692,11 @@ "color-convert": "^1.9.0" } }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, "anymatch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", @@ -1144,6 +1181,15 @@ "unset-value": "^1.0.0" } }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -1507,6 +1553,11 @@ "mimic-response": "^1.0.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -1622,6 +1673,26 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, "convert-source-map": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", @@ -1639,6 +1710,22 @@ } } }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -1910,6 +1997,11 @@ "type-detect": "^4.0.0" } }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2004,6 +2096,16 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2165,6 +2267,11 @@ } } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, "electron": { "version": "6.0.12", "resolved": "https://registry.npmjs.org/electron/-/electron-6.0.12.tgz", @@ -2406,6 +2513,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -2512,6 +2624,11 @@ } } }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, "es-abstract": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", @@ -2555,6 +2672,11 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3378,6 +3500,11 @@ "map-cache": "^0.2.2" } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -4063,12 +4190,33 @@ } } }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + } + }, "http-cache-semantics": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==", "dev": true }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -4443,6 +4591,11 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -4902,6 +5055,14 @@ "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", "dev": true }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -4923,6 +5084,82 @@ "integrity": "sha512-0g5vDDPvNnQk7WM/aE92dTDxXJoOE0biiIcUb3qkn/F6h/ZQZPlZIbE2XSXH2vFPfphkgCxuR2vH6HHnobEOaQ==", "dev": true }, + "koa": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.10.0.tgz", + "integrity": "sha512-vcZopGEWHDokchYtjU6jF1BCy+2MA2hnvGP7xPi26qWoIS0OiAUb4+lCqkqf05qG5ULnGYUFTvFnSK9RyOoiKw==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "error-inject": "^1.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "koa-is-json": "^1.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -5221,6 +5458,11 @@ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, "mem": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", @@ -5368,6 +5610,11 @@ "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -5705,6 +5952,11 @@ "sax": "^1.2.4" } }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", @@ -6329,6 +6581,14 @@ "has": "^1.0.3" } }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6354,6 +6614,11 @@ } } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -6540,6 +6805,11 @@ "@types/node": "*" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -6578,7 +6848,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, "requires": { "isarray": "0.0.1" }, @@ -6586,8 +6855,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" } } }, @@ -8063,6 +8331,11 @@ } } }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, "shallow-equal": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.0.tgz", @@ -8735,6 +9008,11 @@ } } }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, "stdout-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", @@ -9530,6 +9808,11 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -9598,6 +9881,11 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -9626,6 +9914,15 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -9865,6 +10162,11 @@ "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.2.tgz", + "integrity": "sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==" + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -9923,6 +10225,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -10229,6 +10536,11 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" } } } diff --git a/package.json b/package.json index ebfa66b382..a0dc81b378 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ }, "dependencies": { "@inukshuk/exif": "^2.0.0", + "@koa/router": "^8.0.2", "@pixi/filter-adjustment": "^2.7.0", "@tweenjs/tween.js": "^18.3.1", "bluebird": "^3.7.1", @@ -110,6 +111,7 @@ "generic-pool": "^3.7.1", "js-yaml": "^3.13.1", "jsonld": "^1.8.0", + "koa": "^2.10.0", "lodash.debounce": "^4.0.7", "lodash.throttle": "^4.1.1", "memoize-one": "^5.1.1", From f4640281802cce408eea520b53429b8775bb907d Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Tue, 22 Oct 2019 20:34:23 +0200 Subject: [PATCH 07/12] Dispatch API request to project window Resolve API responses --- .eslintrc | 1 + src/actions/activity.js | 3 ++- src/actions/api.js | 26 ++++++++++++++++++++++++++ src/actions/index.js | 1 + src/browser/actions.js | 1 + src/browser/api.js | 32 ++++++++++++++++++++++++++++++-- src/browser/tropy.js | 4 ++++ src/common/api.js | 21 +++++++++++++++++++++ src/sagas/ipc.js | 4 ++-- 9 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 src/actions/api.js diff --git a/.eslintrc b/.eslintrc index 4c22ba42b1..992139b41c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -65,6 +65,7 @@ rules: quote-props: [2, 'consistent-as-needed'] quotes: [2, 'single', 'avoid-escape'] radix: 2 + require-atomic-updates: 0 react/display-name: 0 react/no-deprecated: 1 react/jsx-boolean-value: 2 diff --git a/src/actions/activity.js b/src/actions/activity.js index 59d6b601e6..8c7453aa59 100644 --- a/src/actions/activity.js +++ b/src/actions/activity.js @@ -15,7 +15,8 @@ module.exports = { payload, error, meta: { - ipc: action.meta.ipc, + id: action.meta.id, + ipc: action.meta.api ? 'api' : action.meta.ipc, idx: action.meta.idx, search: action.meta.search, ...meta, diff --git a/src/actions/api.js b/src/actions/api.js new file mode 100644 index 0000000000..884493db88 --- /dev/null +++ b/src/actions/api.js @@ -0,0 +1,26 @@ +'use strict' + +const { ITEM } = require('../constants') +const { array } = require('../common/util') + +module.exports = { + project: { + import({ files, ...payload }, meta) { + return { + type: ITEM.IMPORT, + payload: { + files: array(files), + ...payload + }, + meta: { + api: true, + cmd: 'project', + history: 'add', + search: true, + prompt: false, + ...meta + } + } + } + } +} diff --git a/src/actions/index.js b/src/actions/index.js index ad351e0329..7940fc3318 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -2,6 +2,7 @@ module.exports = { activity: require('./activity'), + api: require('./api'), cache: require('./cache'), classes: require('./classes'), context: require('./context'), diff --git a/src/browser/actions.js b/src/browser/actions.js index 216e22e552..10e9a57de4 100644 --- a/src/browser/actions.js +++ b/src/browser/actions.js @@ -1,6 +1,7 @@ 'use strict' module.exports = { + api: require('../actions/api'), cache: require('../actions/cache'), context: require('../actions/context'), edit: require('../actions/edit'), diff --git a/src/browser/api.js b/src/browser/api.js index ff56de2f3a..c9492a1965 100644 --- a/src/browser/api.js +++ b/src/browser/api.js @@ -1,6 +1,7 @@ 'use strict' -const { info, logger } = require('../common/log') +const { info, warn, logger } = require('../common/log') +const { counter } = require('../common/util') class Server { constructor(app) { @@ -15,9 +16,36 @@ class Server { } this.app = app + this.seq = counter() + this.pending = {} } - dispatch = async () => { + dispatch = (action) => { + return new Promise((resolve, reject) => { + let win = this.app.wm.current() + action.meta.id = this.seq.next().value + + if (this.app.dispatch(action, win)) + this.pending[action.meta.id] = { resolve, reject } + else + reject() + }) + } + + onResponse({ error, payload, meta }) { + try { + if (error) + this.pending[meta.id].reject(payload) + else + this.pending[meta.id].resolve({ payload, meta }) + + } catch (e) { + warn({ + stack: e.stack + }, `failed to resolve API req #${meta.id}: ${e.message}`) + } finally { + delete this.pending[meta.id] + } } start() { diff --git a/src/browser/tropy.js b/src/browser/tropy.js index 979156a41b..518e8fc1e2 100644 --- a/src/browser/tropy.js +++ b/src/browser/tropy.js @@ -716,6 +716,10 @@ class Tropy extends EventEmitter { }) } + ipc.on('api', (event, payload, error, meta) => { + this.api.onResponse({ error, payload, meta }) + }) + ipc.on('cmd', (event, cmd, ...args) => { this.emit(cmd, BrowserWindow.fromWebContents(event.sender), ...args) }) diff --git a/src/common/api.js b/src/common/api.js index 47d0b67a5d..8bf29b22d4 100644 --- a/src/common/api.js +++ b/src/common/api.js @@ -3,6 +3,23 @@ const Koa = require('koa') const Router = require('@koa/router') +const act = require('../actions/api') + +const project = { + async import(ctx) { + let { assert, query, dispatch } = ctx + + assert.ok(query.file, 400, 'missing file parameter') + + let res = await dispatch(act.project.import({ + files: query.file, + list: query.list + })) + + ctx.body = res.payload + } +} + const create = ({ dispatch, log, version }) => { let app = new Koa let api = new Router @@ -19,6 +36,8 @@ const create = ({ dispatch, log, version }) => { app.context.log = log api + .get('/project/import', project.import) + .get('/version', (ctx) => { ctx.body = { version } }) @@ -38,6 +57,8 @@ const logging = async (ctx, next) => { ctx.log.debug({ ms: Date.now() - START, status: ctx.status, + method: ctx.method, + query: ctx.query, url: ctx.url }) } diff --git a/src/sagas/ipc.js b/src/sagas/ipc.js index ba6ffc45ae..1ceb6ac7ec 100644 --- a/src/sagas/ipc.js +++ b/src/sagas/ipc.js @@ -15,12 +15,12 @@ const { TAG, HISTORY } = require('../constants') module.exports = { - *forward(filter, { type, payload, meta }) { + *forward(filter, { type, payload, error, meta }) { try { const event = meta.ipc === true ? type : meta.ipc const data = yield call(filter[event] || identity, payload) - yield call([ipc, ipc.send], event, data) + yield call([ipc, ipc.send], event, data, error, meta) } catch (e) { warn({ stack: e.stack }, 'unexpected error in *ipc:forward') From 701b184f376c88c7206bbea25ba52bff6e2823b4 Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Wed, 23 Oct 2019 10:59:12 +0200 Subject: [PATCH 08/12] Move rsvp functionality to wm --- src/actions/activity.js | 4 +- src/browser/api.js | 36 ++++-------------- src/browser/tropy.js | 12 ++---- src/browser/wm.js | 52 +++++++++++++++++++++++++- src/common/api.js | 7 ++-- src/sagas/ipc.js | 82 +++++++++++++++++++++-------------------- 6 files changed, 111 insertions(+), 82 deletions(-) diff --git a/src/actions/activity.js b/src/actions/activity.js index 8c7453aa59..e50e446976 100644 --- a/src/actions/activity.js +++ b/src/actions/activity.js @@ -15,9 +15,9 @@ module.exports = { payload, error, meta: { - id: action.meta.id, - ipc: action.meta.api ? 'api' : action.meta.ipc, + ipc: action.meta.ipc, idx: action.meta.idx, + rsvp: action.meta.rsvp, search: action.meta.search, ...meta, done: true, diff --git a/src/browser/api.js b/src/browser/api.js index c9492a1965..5bbf60fa0a 100644 --- a/src/browser/api.js +++ b/src/browser/api.js @@ -1,7 +1,6 @@ 'use strict' -const { info, warn, logger } = require('../common/log') -const { counter } = require('../common/util') +const { info, logger } = require('../common/log') class Server { constructor(app) { @@ -11,42 +10,21 @@ class Server { this.koa = api.create({ dispatch: this.dispatch, log: logger.child({ name: 'api' }), + rsvp: this.rsvp, version: app.version }) } this.app = app - this.seq = counter() - this.pending = {} } - dispatch = (action) => { - return new Promise((resolve, reject) => { - let win = this.app.wm.current() - action.meta.id = this.seq.next().value - - if (this.app.dispatch(action, win)) - this.pending[action.meta.id] = { resolve, reject } - else - reject() - }) + dispatch = (type, action) => { + this.app.wm.current(type).webContents.send('dispatch', action) } - onResponse({ error, payload, meta }) { - try { - if (error) - this.pending[meta.id].reject(payload) - else - this.pending[meta.id].resolve({ payload, meta }) - - } catch (e) { - warn({ - stack: e.stack - }, `failed to resolve API req #${meta.id}: ${e.message}`) - } finally { - delete this.pending[meta.id] - } - } + rsvp = (type, action) => ( + this.app.wm.rsvp(type, action) + ) start() { if (this.koa) { diff --git a/src/browser/tropy.js b/src/browser/tropy.js index 518e8fc1e2..32efe19819 100644 --- a/src/browser/tropy.js +++ b/src/browser/tropy.js @@ -399,15 +399,15 @@ class Tropy extends EventEmitter { this.dispatch(act.item.export(target.id, { plugin }), win)) this.on('app:restore-item', (win, { target }) => { - this.dispatch(act.item.restore(target.id)) + this.dispatch(act.item.restore(target.id), win) }) this.on('app:destroy-item', (win, { target }) => { - this.dispatch(act.item.destroy(target.id)) + this.dispatch(act.item.destroy(target.id), win) }) this.on('app:create-item-photo', (win, { target }) => { - this.dispatch(act.photo.create({ item: target.id })) + this.dispatch(act.photo.create({ item: target.id }), win) }) this.on('app:toggle-item-tag', (win, { id, tag }) => { @@ -415,7 +415,7 @@ class Tropy extends EventEmitter { }) this.on('app:clear-item-tags', (win, { id }) => { - this.dispatch(act.item.tags.clear(id)) + this.dispatch(act.item.tags.clear(id), win) }) this.on('app:list-item-remove', (win, { target }) => { @@ -716,10 +716,6 @@ class Tropy extends EventEmitter { }) } - ipc.on('api', (event, payload, error, meta) => { - this.api.onResponse({ error, payload, meta }) - }) - ipc.on('cmd', (event, cmd, ...args) => { this.emit(cmd, BrowserWindow.fromWebContents(event.sender), ...args) }) diff --git a/src/browser/wm.js b/src/browser/wm.js index 07987c5609..e13a15652c 100644 --- a/src/browser/wm.js +++ b/src/browser/wm.js @@ -8,9 +8,18 @@ const { debug, error, trace, warn } = require('../common/log') const { darwin, EL_CAPITAN } = require('../common/os') const { channel } = require('../common/release') const res = require('../common/res') -const { array, blank, get, once, remove, restrict } = require('../common/util') const { BODY, PANEL, ESPER } = require('../constants/sass') +const { + array, + blank, + counter, + get, + once, + remove, + restrict +} = require('../common/util') + const { app, BrowserWindow, @@ -39,6 +48,8 @@ class WindowManager extends EventEmitter { } this.windows = {} + this.pending = {} + this.seq = counter() } broadcast(...args) { @@ -230,11 +241,30 @@ class WindowManager extends EventEmitter { else win.minimize() break + case 'rsvp': + this.handlePendingResponse(...args) + break default: win.emit(type, ...args) } } + handlePendingResponse(action) { + try { + var id = action.meta.rsvp + + if (action.error) + this.pending[id].reject(action.payload) + else + this.pending[id].resolve(action) + + } catch (e) { + warn({ + stack: e.stack + }, `failed to resolve pending message ${id}`) + } + } + handleScrollBarsChange = () => { this.broadcast('scrollbars', !WindowManager.hasOverlayScrollBars()) } @@ -342,6 +372,26 @@ class WindowManager extends EventEmitter { this.each(type, win => win.webContents.send(...args)) } + rsvp(type, action) { + let id + + return new Promise((resolve, reject) => { + id = this.seq.next().value + let win = this.current(type) + + if (win == null) + return reject(new Error(`no ${type} window open`)) + + action.meta.rsvp = id + this.pending[id] = { resolve, reject } + + win.webContents.send('dispatch', action) + + // TODO reject pending after timeout! + + }).finally(() => { delete this.pending[id] }) + } + setTitle(type, title, frameless = false) { if (!frameless || !(darwin && EL_CAPITAN)) { this.each(type, win => win.setTitle(title)) diff --git a/src/common/api.js b/src/common/api.js index 8bf29b22d4..025244af50 100644 --- a/src/common/api.js +++ b/src/common/api.js @@ -7,11 +7,11 @@ const act = require('../actions/api') const project = { async import(ctx) { - let { assert, query, dispatch } = ctx + let { assert, query, rsvp } = ctx assert.ok(query.file, 400, 'missing file parameter') - let res = await dispatch(act.project.import({ + let res = await rsvp('project', act.project.import({ files: query.file, list: query.list })) @@ -20,7 +20,7 @@ const project = { } } -const create = ({ dispatch, log, version }) => { +const create = ({ dispatch, log, rsvp, version }) => { let app = new Koa let api = new Router @@ -34,6 +34,7 @@ const create = ({ dispatch, log, version }) => { app.context.dispatch = dispatch app.context.log = log + app.context.rsvp = rsvp api .get('/project/import', project.import) diff --git a/src/sagas/ipc.js b/src/sagas/ipc.js index 1ceb6ac7ec..20051ea4fc 100644 --- a/src/sagas/ipc.js +++ b/src/sagas/ipc.js @@ -7,49 +7,11 @@ const { const { ipcRenderer: ipc } = require('electron') const { warn } = require('../common/log') -const { identity } = require('../common/util') const history = require('../selectors/history') const { getAllTags } = require('../selectors') const { TAG, HISTORY } = require('../constants') - -module.exports = { - - *forward(filter, { type, payload, error, meta }) { - try { - const event = meta.ipc === true ? type : meta.ipc - const data = yield call(filter[event] || identity, payload) - - yield call([ipc, ipc.send], event, data, error, meta) - - } catch (e) { - warn({ stack: e.stack }, 'unexpected error in *ipc:forward') - } - }, - - *receive() { - const disp = yield call(channel, 'dispatch') - - while (true) { - try { - const action = yield take(disp) - yield put(action) - - } catch (e) { - warn({ stack: e.stack }, 'unexpected error in *ipc:receive') - } - } - }, - - *ipc() { - yield every(({ meta }) => meta && meta.ipc, module.exports.forward, FILTER) - yield fork(module.exports.receive) - } - -} - - -const FILTER = { +const filters = { *[HISTORY.CHANGED]() { const summary = yield select(history.summary) const messages = yield select(state => state.intl.messages) @@ -70,6 +32,39 @@ const FILTER = { } } +function *forward({ type, payload, meta }) { + try { + let name = meta.ipc === true ? type : meta.ipc + + let data = (name in filters) ? + yield call(filters[name], payload) : + payload + + yield call([ipc, ipc.send], name, data) + + } catch (e) { + warn({ stack: e.stack }, 'unexpected error in *ipc:forward') + } +} + +function *rsvp(action) { + try { + yield call([ipc, ipc.send], 'wm', 'rsvp', action) + + } catch (e) { + warn({ stack: e.stack }, 'unexpected error in *ipc:rsvp') + } +} + +function *receive() { + let dispatches = yield call(channel, 'dispatch') + + while (true) { + let action = yield take(dispatches) + yield put(action) + } +} + function channel(name) { return eventChannel(emitter => { const listener = (_, ...actions) => { @@ -85,3 +80,12 @@ function channel(name) { return () => ipc.removeListener(name, listener) }) } + +module.exports = { + *ipc() { + yield every(({ error, meta }) => !error && meta && meta.ipc, forward) + yield every(({ meta }) => meta && meta.rsvp && meta.done, rsvp) + + yield fork(receive) + } +} From f384dbb6600dcd8c6642551062a86076700018e9 Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Wed, 23 Oct 2019 11:13:55 +0200 Subject: [PATCH 09/12] Add default port and use state.api flag --- src/browser/api.js | 27 +++++++++++++-------------- src/browser/tropy.js | 1 + 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/browser/api.js b/src/browser/api.js index 5bbf60fa0a..f15c5786be 100644 --- a/src/browser/api.js +++ b/src/browser/api.js @@ -4,17 +4,6 @@ const { info, logger } = require('../common/log') class Server { constructor(app) { - if (app.opts.port) { - let api = require('../common/api') - - this.koa = api.create({ - dispatch: this.dispatch, - log: logger.child({ name: 'api' }), - rsvp: this.rsvp, - version: app.version - }) - } - this.app = app } @@ -27,9 +16,19 @@ class Server { ) start() { - if (this.koa) { - let { port } = this.app.opts - info(`starting api on port ${port}`) + if (this.app.state.api || this.app.opts.port) { + let api = require('../common/api') + + this.koa = api.create({ + dispatch: this.dispatch, + log: logger.child({ name: 'api' }), + rsvp: this.rsvp, + version: this.app.version + }) + + let port = this.app.opts.port || this.app.state.port + + info(`api.start on port ${port}`) this.koa.listen(port) } } diff --git a/src/browser/tropy.js b/src/browser/tropy.js index 32efe19819..4cbe453182 100644 --- a/src/browser/tropy.js +++ b/src/browser/tropy.js @@ -54,6 +54,7 @@ class Tropy extends EventEmitter { static defaults = { frameless: darwin, debug: false, + port: 2019, theme: darwin ? 'system' : 'light', recent: [], updater: true, From 16e49e2201157ef7144800915d4c5bc4b9a52f9e Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Wed, 23 Oct 2019 21:38:27 +0200 Subject: [PATCH 10/12] Add item find and show routes Prototype implementation for item.show --- src/actions/api.js | 44 +++++++++++++++++++++++++++++++--------- src/commands/api.js | 46 ++++++++++++++++++++++++++++++++++++++++++ src/commands/index.js | 1 + src/common/api.js | 33 +++++++++++++++++++++++++++--- src/constants/api.js | 8 ++++++++ src/constants/index.js | 1 + 6 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 src/commands/api.js create mode 100644 src/constants/api.js diff --git a/src/actions/api.js b/src/actions/api.js index 884493db88..5bf0cb8a4c 100644 --- a/src/actions/api.js +++ b/src/actions/api.js @@ -1,23 +1,47 @@ 'use strict' -const { ITEM } = require('../constants') +const { API, ITEM } = require('../constants') const { array } = require('../common/util') module.exports = { - project: { - import({ files, ...payload }, meta) { + import({ files, ...payload }, meta) { + return { + type: ITEM.IMPORT, + payload: { + ...payload, + files: array(files) + }, + meta: { + cmd: 'project', + history: 'add', + search: true, + prompt: false, + ...meta + } + } + }, + + item: { + find({ tags, ...payload }, meta) { return { - type: ITEM.IMPORT, + type: API.ITEM.FIND, payload: { - files: array(files), - ...payload + ...payload, + tags: array(tags) }, meta: { - api: true, cmd: 'project', - history: 'add', - search: true, - prompt: false, + ...meta + } + } + }, + + show(payload, meta) { + return { + type: API.ITEM.SHOW, + payload, + meta: { + cmd: 'project', ...meta } } diff --git a/src/commands/api.js b/src/commands/api.js new file mode 100644 index 0000000000..39c985271e --- /dev/null +++ b/src/commands/api.js @@ -0,0 +1,46 @@ +'use strict' + +const { select } = require('redux-saga/effects') +const { Command } = require('./command') +const { API } = require('../constants') +const { pick } = require('../common/util') +//const act = require('../actions') +//const mod = require('../models') + + +class Find extends Command { + static get ACTION() { + return API.ITEM.FIND + } + + *exec() { + } +} + +class Show extends Command { + static get ACTION() { + return API.ITEM.SHOW + } + + *exec() { + let { id } = this.action.payload + + let [item, data] = yield select(state => ([ + state.items[id], + state.metadata[id] + ])) + + if (item == null) + return null + + let photos = yield select(state => + pick(state.photos, item.photos)) + + return { ...item, data, photos } + } +} + +module.exports = { + Find, + Show +} diff --git a/src/commands/index.js b/src/commands/index.js index 0f1fd9120d..9ac6eb6ffd 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -6,6 +6,7 @@ const handles = map(([, cmd]) => [cmd.ACTION, cmd]) module.exports = { ...seq(require('./cache'), handles), + ...seq(require('./api'), handles), ...seq(require('./item'), handles), ...seq(require('./list'), handles), ...seq(require('./metadata'), handles), diff --git a/src/common/api.js b/src/common/api.js index 025244af50..c71c7806ba 100644 --- a/src/common/api.js +++ b/src/common/api.js @@ -2,7 +2,6 @@ const Koa = require('koa') const Router = require('@koa/router') - const act = require('../actions/api') const project = { @@ -11,12 +10,37 @@ const project = { assert.ok(query.file, 400, 'missing file parameter') - let res = await rsvp('project', act.project.import({ + let { payload } = await rsvp('project', act.import({ files: query.file, list: query.list })) - ctx.body = res.payload + ctx.body = payload + }, + + items: { + async find(ctx) { + let { query, rsvp } = ctx + + let { payload } = await rsvp('project', act.item.find({ + tags: query.tag + })) + + ctx.body = payload + }, + + async show(ctx) { + let { params, rsvp } = ctx + + let { payload } = await rsvp('project', act.item.show({ + id: params.id + })) + + if (payload != null) + ctx.body = payload + else + ctx.status = 404 + } } } @@ -39,6 +63,9 @@ const create = ({ dispatch, log, rsvp, version }) => { api .get('/project/import', project.import) + .get('/project/items', project.items.find) + .get('/project/items/:id', project.items.show) + .get('/version', (ctx) => { ctx.body = { version } }) diff --git a/src/constants/api.js b/src/constants/api.js new file mode 100644 index 0000000000..595473f6da --- /dev/null +++ b/src/constants/api.js @@ -0,0 +1,8 @@ +'use strict' + +module.exports = { + ITEM: { + FIND: 'api.item.find', + SHOW: 'api.item.show' + } +} diff --git a/src/constants/index.js b/src/constants/index.js index 32241a402d..00c5808862 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -2,6 +2,7 @@ module.exports = { ACTIVITY: require('./activity'), + API: require('./api'), CACHE: require('./cache'), CONTEXT: require('./context'), DND: require('./dnd'), From 0fa99c9e3bfece71333ed0a031856b47b77c86a2 Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Fri, 25 Oct 2019 09:44:14 +0200 Subject: [PATCH 11/12] Add prototype API photo and selection endpoints --- src/actions/api.js | 26 +++++++++++++++++++++ src/commands/api.js | 54 +++++++++++++++++++++++++++++++++----------- src/common/api.js | 34 ++++++++++++++++++++-------- src/constants/api.js | 17 ++++++++++++++ 4 files changed, 108 insertions(+), 23 deletions(-) diff --git a/src/actions/api.js b/src/actions/api.js index 5bf0cb8a4c..b934cd2e16 100644 --- a/src/actions/api.js +++ b/src/actions/api.js @@ -46,5 +46,31 @@ module.exports = { } } } + }, + + photo: { + show(payload, meta) { + return { + type: API.PHOTO.SHOW, + payload, + meta: { + cmd: 'project', + ...meta + } + } + } + }, + + selection: { + show(payload, meta) { + return { + type: API.SELECTION.SHOW, + payload, + meta: { + cmd: 'project', + ...meta + } + } + } } } diff --git a/src/commands/api.js b/src/commands/api.js index 39c985271e..f62092bb11 100644 --- a/src/commands/api.js +++ b/src/commands/api.js @@ -3,12 +3,12 @@ const { select } = require('redux-saga/effects') const { Command } = require('./command') const { API } = require('../constants') -const { pick } = require('../common/util') +const { pluck } = require('../common/util') //const act = require('../actions') //const mod = require('../models') -class Find extends Command { +class ItemFind extends Command { static get ACTION() { return API.ITEM.FIND } @@ -17,7 +17,7 @@ class Find extends Command { } } -class Show extends Command { +class ItemShow extends Command { static get ACTION() { return API.ITEM.SHOW } @@ -25,22 +25,50 @@ class Show extends Command { *exec() { let { id } = this.action.payload - let [item, data] = yield select(state => ([ - state.items[id], - state.metadata[id] - ])) + let { items, metadata, photos, notes } = yield select() - if (item == null) + if (!(id in items)) return null - let photos = yield select(state => - pick(state.photos, item.photos)) + return { + ...items[id], + data: metadata[id], + photos: pluck(photos, items[id].photos).map(photo => ({ + ...photo, + notes: pluck(notes, photo.notes) + })) + } + } +} + +class PhotoShow extends Command { + static get ACTION() { + return API.PHOTO.SHOW + } + + *exec() { + let { id } = this.action.payload + let photo = yield select(state => state.photos[id]) + return photo + } +} + +class SelectionShow extends Command { + static get ACTION() { + return API.SELECTION.SHOW + } - return { ...item, data, photos } + *exec() { + let { id } = this.action.payload + let selection = yield select(state => state.selections[id]) + return selection } } + module.exports = { - Find, - Show + ItemFind, + ItemShow, + PhotoShow, + SelectionShow } diff --git a/src/common/api.js b/src/common/api.js index c71c7806ba..20cc03b9c0 100644 --- a/src/common/api.js +++ b/src/common/api.js @@ -4,6 +4,20 @@ const Koa = require('koa') const Router = require('@koa/router') const act = require('../actions/api') +const show = (type) => + async (ctx) => { + let { params, rsvp } = ctx + + let { payload } = await rsvp('project', act[type].show({ + id: params.id + })) + + if (payload != null) + ctx.body = payload + else + ctx.status = 404 + } + const project = { async import(ctx) { let { assert, query, rsvp } = ctx @@ -29,18 +43,15 @@ const project = { ctx.body = payload }, - async show(ctx) { - let { params, rsvp } = ctx + show: show('item') + }, - let { payload } = await rsvp('project', act.item.show({ - id: params.id - })) + photos: { + show: show('photo') + }, - if (payload != null) - ctx.body = payload - else - ctx.status = 404 - } + selections: { + show: show('selection') } } @@ -66,6 +77,9 @@ const create = ({ dispatch, log, rsvp, version }) => { .get('/project/items', project.items.find) .get('/project/items/:id', project.items.show) + .get('/project/photos/:id', project.photos.show) + .get('/project/selections/:id', project.photos.show) + .get('/version', (ctx) => { ctx.body = { version } }) diff --git a/src/constants/api.js b/src/constants/api.js index 595473f6da..68ef02570e 100644 --- a/src/constants/api.js +++ b/src/constants/api.js @@ -4,5 +4,22 @@ module.exports = { ITEM: { FIND: 'api.item.find', SHOW: 'api.item.show' + }, + + PHOTO: { + SHOW: 'api.photo.show' + }, + + SELECTION: { + SHOW: 'api.selection.show' + }, + + NOTE: { + SHOW: 'api.note.show' + }, + + TAG: { + LIST: 'api.tag.list', + SHOW: 'api.tag.show' } } From 09c561019e95d2fb70228e74f9e8f0cd115c7208 Mon Sep 17 00:00:00 2001 From: Sylvester Keil Date: Fri, 25 Oct 2019 11:04:15 +0200 Subject: [PATCH 12/12] Add API prototype for note and item/photos --- src/actions/api.js | 24 ++++++++++++++++++ src/commands/api.js | 58 ++++++++++++++++++++++++++++++++++++-------- src/common/api.js | 53 +++++++++++++++++++++++++++++++++++++--- src/constants/api.js | 1 + 4 files changed, 122 insertions(+), 14 deletions(-) diff --git a/src/actions/api.js b/src/actions/api.js index b934cd2e16..ec7bd21406 100644 --- a/src/actions/api.js +++ b/src/actions/api.js @@ -48,7 +48,31 @@ module.exports = { } }, + note: { + show(payload, meta) { + return { + type: API.NOTE.SHOW, + payload, + meta: { + cmd: 'project', + ...meta + } + } + } + }, + photo: { + find(payload, meta) { + return { + type: API.PHOTO.FIND, + payload, + meta: { + cmd: 'project', + ...meta + } + } + }, + show(payload, meta) { return { type: API.PHOTO.SHOW, diff --git a/src/commands/api.js b/src/commands/api.js index f62092bb11..8448c63f62 100644 --- a/src/commands/api.js +++ b/src/commands/api.js @@ -4,6 +4,7 @@ const { select } = require('redux-saga/effects') const { Command } = require('./command') const { API } = require('../constants') const { pluck } = require('../common/util') +const { serialize } = require('../export/note') //const act = require('../actions') //const mod = require('../models') @@ -24,23 +25,58 @@ class ItemShow extends Command { *exec() { let { id } = this.action.payload + let item = yield select(state => state.items[id]) + return item + } +} + +class NoteShow extends Command { + static get ACTION() { + return API.NOTE.SHOW + } + + *exec() { + let { id, format } = this.action.payload - let { items, metadata, photos, notes } = yield select() + let note = yield select(state => state.notes[id]) - if (!(id in items)) - return null + if (note == null) + return null - return { - ...items[id], - data: metadata[id], - photos: pluck(photos, items[id].photos).map(photo => ({ - ...photo, - notes: pluck(notes, photo.notes) - })) + switch (format) { + case 'html': + return serialize(note, { format: { html: true }, localize: false }).html + case 'plain': + case 'text': + return note.text + case 'md': + case 'markdown': + return serialize(note, { + format: { markdown: true }, + localize: false + }).markdown + default: + return note } } } +class PhotoFind extends Command { + static get ACTION() { + return API.PHOTO.FIND + } + + *exec() { + let { item } = this.action.payload + let { items, photos } = yield select() + + if (!(item in items)) + return null + + return pluck(photos, items[item].photos) + } +} + class PhotoShow extends Command { static get ACTION() { return API.PHOTO.SHOW @@ -69,6 +105,8 @@ class SelectionShow extends Command { module.exports = { ItemFind, ItemShow, + NoteShow, + PhotoFind, PhotoShow, SelectionShow } diff --git a/src/common/api.js b/src/common/api.js index 20cc03b9c0..24613b11e9 100644 --- a/src/common/api.js +++ b/src/common/api.js @@ -9,7 +9,7 @@ const show = (type) => let { params, rsvp } = ctx let { payload } = await rsvp('project', act[type].show({ - id: params.id + id: params[type] })) if (payload != null) @@ -46,7 +46,49 @@ const project = { show: show('item') }, + notes: { + async show(ctx) { + let { assert, params, query, rsvp } = ctx + + if (query.format) + assert( + (/^(json|html|plain|text|md|markdown)$/).test(query.format), + 400, + 'format unknown') + + let { payload } = await rsvp('project', act.note.show({ + id: params.note, + format: query.format + })) + + if (payload != null) { + if (query.format === 'html') + ctx.type = 'text/html' + if (query.format === 'markdown' || query.format === 'md') + ctx.type = 'text/markdown' + + ctx.body = payload + + } else { + ctx.status = 404 + } + } + }, + photos: { + async find(ctx) { + let { params, rsvp } = ctx + + let { payload } = await rsvp('project', act.photo.find({ + item: params.item + })) + + if (payload != null) + ctx.body = payload + else + ctx.status = 404 + }, + show: show('photo') }, @@ -75,10 +117,13 @@ const create = ({ dispatch, log, rsvp, version }) => { .get('/project/import', project.import) .get('/project/items', project.items.find) - .get('/project/items/:id', project.items.show) + .get('/project/items/:item', project.items.show) + .get('/project/items/:item/photos', project.photos.find) + + .get('/project/notes/:note', project.notes.show) + .get('/project/photos/:photo', project.photos.show) + .get('/project/selections/:selection', project.photos.show) - .get('/project/photos/:id', project.photos.show) - .get('/project/selections/:id', project.photos.show) .get('/version', (ctx) => { ctx.body = { version } diff --git a/src/constants/api.js b/src/constants/api.js index 68ef02570e..0760f4a94e 100644 --- a/src/constants/api.js +++ b/src/constants/api.js @@ -7,6 +7,7 @@ module.exports = { }, PHOTO: { + FIND: 'api.photo.find', SHOW: 'api.photo.show' },