Display link to resources and display them via server.

This commit is contained in:
Laurent Cozic
2017-10-24 20:40:15 +01:00
parent 3cb52a4107
commit 13b4f3f4ea
15 changed files with 228 additions and 34 deletions
+85
View File
@@ -0,0 +1,85 @@
import { _ } from 'lib/locale.js'
import { Logger } from 'lib/logger.js';
import { Resource } from 'lib/models/resource.js';
import { netUtils } from 'lib/net-utils.js'
const http = require("http");
const urlParser = require("url");
const enableServerDestroy = require('server-destroy');
class ResourceServer {
constructor() {
this.server_ = null;
this.logger_ = new Logger();
this.port_ = null;
}
setLogger(logger) {
this.logger_ = logger;
}
logger() {
return this.logger_;
}
baseUrl() {
if (!this.port_) return '';
return 'http://127.0.0.1:' + this.port_;
}
async start() {
this.port_ = await netUtils.findAvailablePort([9167, 9267, 8167, 8267]);
if (!this.port_) {
this.logger().error('Could not find available port to start resource server. Please report the error at https://github.com/laurent22/joplin');
return;
}
this.server_ = http.createServer();
this.server_.on('request', async (request, response) => {
const writeResponse = (message) => {
response.write(message);
response.end();
}
const url = urlParser.parse(request.url, true);
let resourceId = url.pathname.split('/');
if (resourceId.length < 2) {
writeResponse('Error: could not get resource ID from path name: ' + url.pathname);
return;
}
resourceId = resourceId[1];
try {
let resource = await Resource.loadByPartialId(resourceId);
if (!resource.length) throw new Error('No resource with ID ' + resourceId);
if (resource.length > 2) throw new Error('More than one resource match ID ' + resourceId); // That's very unlikely
resource = resource[0];
if (resource.mime) response.setHeader('Content-Type', resource.mime);
writeResponse(await Resource.content(resource));
} catch (error) {
response.setHeader('Content-Type', 'text/plain');
response.statusCode = 400;
writeResponse('Error: could not retrieve resource: ' + error.message);
}
});
this.server_.on('error', (error) => {
this.logger().error('Resource server:', error);
});
this.server_.listen(this.port_);
enableServerDestroy(this.server_);
}
stop() {
if (this.server_) this.server_.destroy();
this.server_ = null;
}
}
module.exports = ResourceServer;
+64 -6
View File
@@ -23,6 +23,7 @@ const RootWidget = require('tkwidgets/RootWidget.js');
const WindowWidget = require('tkwidgets/WindowWidget.js'); const WindowWidget = require('tkwidgets/WindowWidget.js');
const NoteWidget = require('./gui/NoteWidget.js'); const NoteWidget = require('./gui/NoteWidget.js');
const ResourceServer = require('./ResourceServer.js');
const NoteMetadataWidget = require('./gui/NoteMetadataWidget.js'); const NoteMetadataWidget = require('./gui/NoteMetadataWidget.js');
const FolderListWidget = require('./gui/FolderListWidget.js'); const FolderListWidget = require('./gui/FolderListWidget.js');
const NoteListWidget = require('./gui/NoteListWidget.js'); const NoteListWidget = require('./gui/NoteListWidget.js');
@@ -229,6 +230,10 @@ class AppGui {
this.rootWidget_.addChild(win1); this.rootWidget_.addChild(win1);
} }
addCommandToConsole(cmd) {
this.stdout(chalk.cyan.bold('> ' + cmd));
}
setupShortcuts() { setupShortcuts() {
const shortcuts = {}; const shortcuts = {};
@@ -291,7 +296,7 @@ class AppGui {
action: async () => { action: async () => {
const cmd = await this.widget('statusBar').prompt(); const cmd = await this.widget('statusBar').prompt();
if (!cmd) return; if (!cmd) return;
this.stdout(chalk.cyan.bold('> ' + cmd)); this.addCommandToConsole(cmd);
await this.processCommand(cmd); await this.processCommand(cmd);
}, },
}; };
@@ -327,17 +332,17 @@ class AppGui {
shortcuts['nn'] = { shortcuts['nn'] = {
description: _('Create a new note'), description: _('Create a new note'),
action: { type: 'prompt', initialText: 'mknote ' }, action: { type: 'prompt', initialText: 'mknote ""', cursorPosition: -2 },
} }
shortcuts['nt'] = { shortcuts['nt'] = {
description: _('Create a new todo'), description: _('Create a new todo'),
action: { type: 'prompt', initialText: 'mktodo ' }, action: { type: 'prompt', initialText: 'mktodo ""', cursorPosition: -2 },
} }
shortcuts['nb'] = { shortcuts['nb'] = {
description: _('Create a new notebook'), description: _('Create a new notebook'),
action: { type: 'prompt', initialText: 'mkbook ' }, action: { type: 'prompt', initialText: 'mkbook ""', cursorPosition: -2 },
} }
return shortcuts; return shortcuts;
@@ -526,6 +531,11 @@ class AppGui {
this.updateStatusBarMessage(); this.updateStatusBarMessage();
} }
exit() {
this.fullScreen(false);
this.resourceServer_.stop();
}
updateStatusBarMessage() { updateStatusBarMessage() {
const consoleWidget = this.widget('console'); const consoleWidget = this.widget('console');
@@ -547,12 +557,58 @@ class AppGui {
if (msg !== '') this.widget('statusBar').setItemAt(0, msg); if (msg !== '') this.widget('statusBar').setItemAt(0, msg);
} }
setupResourceServer() {
const linkStyle = chalk.blue.underline;
const noteTextWidget = this.widget('noteText');
const resourceIdRegex = /^:\/[a-f0-9]+$/i
const regularUrlRenderer = (url) => {
if (!url) return url;
const l = url.toLowerCase();
if (l.indexOf('http://') === 0 || l.indexOf('https://') === 0 || l.indexOf('www.') === 0) {
return linkStyle(url);
}
return url;
}
// By default, before the server is started, only the regular
// URLs appear in blue.
noteTextWidget.markdownRendererOptions = {
linkUrlRenderer: (url) => {
if (resourceIdRegex.test(url)) {
return url;
} else {
return regularUrlRenderer(url);
}
},
};
this.resourceServer_ = new ResourceServer();
this.resourceServer_.setLogger(this.app().logger());
this.resourceServer_.start().then(() => {
if (this.resourceServer_.baseUrl()) {
noteTextWidget.markdownRendererOptions = {
linkUrlRenderer: (url) => {
if (resourceIdRegex.test(url)) {
const resourceId = url.substr(2);
return linkStyle(this.resourceServer_.baseUrl() + '/' + resourceId.substr(0, 7));
} else {
return regularUrlRenderer(url);
}
},
};
}
});
}
async start() { async start() {
const term = this.term(); const term = this.term();
this.fullScreen(); this.fullScreen();
try { try {
this.setupResourceServer();
this.renderer_.start(); this.renderer_.start();
const statusBar = this.widget('statusBar'); const statusBar = this.widget('statusBar');
@@ -630,8 +686,10 @@ class AppGui {
await cmd.action(); await cmd.action();
} else if (typeof cmd.action === 'object') { } else if (typeof cmd.action === 'object') {
if (cmd.action.type === 'prompt') { if (cmd.action.type === 'prompt') {
const commandString = await statusBar.prompt(cmd.action.initialText ? cmd.action.initialText : ''); let promptOptions = {};
this.stdout(commandString); if ('cursorPosition' in cmd.action) promptOptions.cursorPosition = cmd.action.cursorPosition;
const commandString = await statusBar.prompt(cmd.action.initialText ? cmd.action.initialText : '', null, promptOptions);
this.addCommandToConsole(commandString);
await this.processCommand(commandString); await this.processCommand(commandString);
} else { } else {
throw new Error('Unknown command: ' + JSON.stringify(cmd.action)); throw new Error('Unknown command: ' + JSON.stringify(cmd.action));
+19 -3
View File
@@ -315,9 +315,24 @@ class Application {
} }
async exit(code = 0) { async exit(code = 0) {
await Setting.saveAll(); const doExit = async () => {
this.gui().fullScreen(false); await Setting.saveAll();
process.exit(code); this.gui().exit();
process.exit(code);
};
// Give it a few seconds to cancel otherwise exit anyway
setTimeout(async () => {
await doExit();
}, 5000);
if (await reg.syncStarted()) {
this.stdout(_('Cancelling background synchronisation... Please wait.'));
const sync = await reg.synchronizer(Setting.value('sync.target'));
await sync.cancel();
}
await doExit();
} }
commands() { commands() {
@@ -403,6 +418,7 @@ class Application {
maximizeConsole: () => {}, maximizeConsole: () => {},
stdout: (text) => { console.info(text); }, stdout: (text) => { console.info(text); },
fullScreen: (b=true) => {}, fullScreen: (b=true) => {},
exit: () => {},
}; };
} }
+1 -1
View File
@@ -63,7 +63,7 @@ class Command extends BaseCommand {
await Resource.save(resource, { isNew: true }); await Resource.save(resource, { isNew: true });
note.body += "\n" + Resource.markdownTag(resource); note.body += "\n\n" + Resource.markdownTag(resource);
await Note.save(note); await Note.save(note);
} }
+1 -1
View File
@@ -32,7 +32,7 @@ class FolderListWidget extends ListWidget {
output.push(item.title); output.push(item.title);
} }
if (item && item.id) output.push(item.id.substr(0, 5)); // if (item && item.id) output.push(item.id.substr(0, 5));
return output.join(' '); return output.join(' ');
}; };
+7 -1
View File
@@ -27,8 +27,10 @@ class StatusBarWidget extends BaseWidget {
this.invalidate(); this.invalidate();
} }
async prompt(initialText = '', promptString = ':') { async prompt(initialText = '', promptString = null, options = null) {
if (this.promptState_) throw new Error('Another prompt already active'); if (this.promptState_) throw new Error('Another prompt already active');
if (promptString === null) promptString = ':';
if (options === null) options = {};
this.root.globalDisableKeyboard(this); this.root.globalDisableKeyboard(this);
@@ -38,6 +40,8 @@ class StatusBarWidget extends BaseWidget {
promptString: stripAnsi(promptString), promptString: stripAnsi(promptString),
}; };
if ('cursorPosition' in options) this.promptState_.cursorPosition = options.cursorPosition;
this.promptState_.promise = new Promise((resolve, reject) => { this.promptState_.promise = new Promise((resolve, reject) => {
this.promptState_.resolve = resolve; this.promptState_.resolve = resolve;
this.promptState_.reject = reject; this.promptState_.reject = reject;
@@ -100,6 +104,8 @@ class StatusBarWidget extends BaseWidget {
style: this.term.innerStyle.bgBrightBlue.white, // NOTE: Need to use TK style for this as inputField is not compatible with chalk style: this.term.innerStyle.bgBrightBlue.white, // NOTE: Need to use TK style for this as inputField is not compatible with chalk
}; };
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
this.inputEventEmitter_ = this.term.inputField(options, (error, input) => { this.inputEventEmitter_ = this.term.inputField(options, (error, input) => {
let resolveResult = null; let resolveResult = null;
const resolveFn = this.promptState_.resolve; const resolveFn = this.promptState_.resolve;
+2 -11
View File
@@ -1,6 +1,6 @@
import { _ } from 'lib/locale.js' import { _ } from 'lib/locale.js'
import { netUtils } from 'lib/net-utils.js'
const tcpPortUsed = require('tcp-port-used');
const http = require("http"); const http = require("http");
const urlParser = require("url"); const urlParser = require("url");
const FormData = require('form-data'); const FormData = require('form-data');
@@ -43,16 +43,7 @@ class OneDriveApiNodeUtils {
this.api().setAuth(null); this.api().setAuth(null);
let ports = this.possibleOAuthDancePorts(); const port = netUtils.findAvailablePort(this.possibleOAuthDancePorts());
let port = null;
for (let i = 0; i < ports.length; i++) {
let inUse = await tcpPortUsed.check(ports[i]);
if (!inUse) {
port = ports[i];
break;
}
}
if (!port) throw new Error(_('All potential ports are in use - please report the issue at %s', 'https://github.com/laurent22/joplin')); if (!port) throw new Error(_('All potential ports are in use - please report the issue at %s', 'https://github.com/laurent22/joplin'));
let authCodeUrl = this.api().authCodeUrl('http://localhost:' + port); let authCodeUrl = this.api().authCodeUrl('http://localhost:' + port);
+3
View File
@@ -90,6 +90,9 @@ msgstr ""
msgid "Exits the application." msgid "Exits the application."
msgstr "" msgstr ""
msgid "Cancelling background synchronisation... Please wait."
msgstr ""
msgid "Only Bash is currently supported for autocompletion." msgid "Only Bash is currently supported for autocompletion."
msgstr "" msgstr ""
+4
View File
@@ -100,6 +100,10 @@ msgstr "Affiche l'aide pour la commande donnée."
msgid "Exits the application." msgid "Exits the application."
msgstr "Quitter le logiciel." msgstr "Quitter le logiciel."
#, fuzzy
msgid "Cancelling background synchronisation... Please wait."
msgstr "Annulation..."
msgid "Only Bash is currently supported for autocompletion." msgid "Only Bash is currently supported for autocompletion."
msgstr "" msgstr ""
+3
View File
@@ -90,6 +90,9 @@ msgstr ""
msgid "Exits the application." msgid "Exits the application."
msgstr "" msgstr ""
msgid "Cancelling background synchronisation... Please wait."
msgstr ""
msgid "Only Bash is currently supported for autocompletion." msgid "Only Bash is currently supported for autocompletion."
msgstr "" msgstr ""
+8 -8
View File
@@ -14,8 +14,8 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1" json-stable-stringify "^1.0.1"
ajv@^5.1.0: ajv@^5.1.0:
version "5.2.4" version "5.2.5"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.4.tgz#3daf9a8b67221299fdae8d82d117ed8e6c80244b" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.5.tgz#b637234d3e2675eb5f79fc652242a853a48cb49f"
dependencies: dependencies:
co "^4.6.0" co "^4.6.0"
fast-deep-equal "^1.0.0" fast-deep-equal "^1.0.0"
@@ -832,8 +832,8 @@ chalk@^1.1.3:
supports-color "^2.0.0" supports-color "^2.0.0"
chalk@^2.1.0: chalk@^2.1.0:
version "2.2.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.2.0.tgz#477b3bf2f9b8fd5ca9e429747e37f724ee7af240" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
dependencies: dependencies:
ansi-styles "^3.1.0" ansi-styles "^3.1.0"
escape-string-regexp "^1.0.5" escape-string-regexp "^1.0.5"
@@ -1815,9 +1815,9 @@ node-bitmap@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/node-bitmap/-/node-bitmap-0.0.1.tgz#180eac7003e0c707618ef31368f62f84b2a69091" resolved "https://registry.yarnpkg.com/node-bitmap/-/node-bitmap-0.0.1.tgz#180eac7003e0c707618ef31368f62f84b2a69091"
"node-emoji@https://github.com/laurent22/node-emoji": "node-emoji@git+https://github.com/laurent22/node-emoji.git":
version "1.8.1" version "1.8.1"
resolved "https://github.com/laurent22/node-emoji#9fa01eac463e94dde1316ef8c53089eeef4973b5" resolved "git+https://github.com/laurent22/node-emoji.git#9fa01eac463e94dde1316ef8c53089eeef4973b5"
dependencies: dependencies:
lodash.toarray "^4.4.0" lodash.toarray "^4.4.0"
@@ -2455,8 +2455,8 @@ tcp-port-used@^0.1.2:
q "0.9.7" q "0.9.7"
terminal-kit@^1.13.11: terminal-kit@^1.13.11:
version "1.13.13" version "1.14.0"
resolved "https://registry.yarnpkg.com/terminal-kit/-/terminal-kit-1.13.13.tgz#fa53dbc17f5fb7e1630784274a7e57a10b1c0201" resolved "https://registry.yarnpkg.com/terminal-kit/-/terminal-kit-1.14.0.tgz#1cb89c22bf697152a3f8e54569c61d5084ed690a"
dependencies: dependencies:
async-kit "^2.2.3" async-kit "^2.2.3"
get-pixels "^3.3.0" get-pixels "^3.3.0"
+9
View File
@@ -109,6 +109,15 @@ class BaseModel {
return id.substr(0, 5); return id.substr(0, 5);
} }
// static minimalPartialId(id) {
// let length = 2;
// while (true) {
// const partialId = id.substr(0, length);
// const r = await this.db().selectOne('SELECT count(*) as total FROM `' + this.tableName() + '` WHERE `id` LIKE ?', [partialId + '%']);
// if (r['total'] <= 1) return partialId;
// }
// }
static loadByPartialId(partialId) { static loadByPartialId(partialId) {
return this.modelSelectAll('SELECT * FROM `' + this.tableName() + '` WHERE `id` LIKE ?', [partialId + '%']); return this.modelSelectAll('SELECT * FROM `' + this.tableName() + '` WHERE `id` LIKE ?', [partialId + '%']);
} }
+2 -2
View File
@@ -5,7 +5,7 @@ import { mime } from 'lib/mime-utils.js';
import { filename } from 'lib/path-utils.js'; import { filename } from 'lib/path-utils.js';
import { FsDriverDummy } from 'lib/fs-driver-dummy.js'; import { FsDriverDummy } from 'lib/fs-driver-dummy.js';
import { markdownUtils } from 'lib/markdown-utils.js'; import { markdownUtils } from 'lib/markdown-utils.js';
import lodash from 'lodash'; import lodash from 'lodash';
class Resource extends BaseItem { class Resource extends BaseItem {
@@ -59,7 +59,7 @@ class Resource extends BaseItem {
return filename(path); return filename(path);
} }
static content(resource) { static async content(resource) {
return this.fsDriver().readFile(this.fullPath(resource)); return this.fsDriver().readFile(this.fullPath(resource));
} }
+18
View File
@@ -1,5 +1,7 @@
import { shim } from 'lib/shim.js' import { shim } from 'lib/shim.js'
const tcpPortUsed = require('tcp-port-used');
const netUtils = {}; const netUtils = {};
netUtils.ip = async () => { netUtils.ip = async () => {
@@ -12,4 +14,20 @@ netUtils.ip = async () => {
return ip.ip; return ip.ip;
} }
netUtils.findAvailablePort = async (possiblePorts, extraRandomPortsToTry = 20) => {
for (let i = 0; i < extraRandomPortsToTry; i++) {
possiblePorts.push(Math.floor(8000 + Math.random() * 2000));
}
let port = null;
for (let i = 0; i < possiblePorts.length; i++) {
let inUse = await tcpPortUsed.check(possiblePorts[i]);
if (!inUse) {
port = possiblePorts[i];
break;
}
}
return port;
}
export { netUtils }; export { netUtils };
+2 -1
View File
@@ -14,6 +14,7 @@ import { _ } from 'lib/locale.js';
const reg = {}; const reg = {};
reg.initSynchronizerStates_ = {}; reg.initSynchronizerStates_ = {};
reg.synchronizers_ = {};
reg.logger = () => { reg.logger = () => {
if (!reg.logger_) { if (!reg.logger_) {
@@ -96,7 +97,6 @@ reg.initSynchronizer_ = async (syncTargetId) => {
} }
reg.synchronizer = async (syncTargetId) => { reg.synchronizer = async (syncTargetId) => {
if (!reg.synchronizers_) reg.synchronizers_ = [];
if (reg.synchronizers_[syncTargetId]) return reg.synchronizers_[syncTargetId]; if (reg.synchronizers_[syncTargetId]) return reg.synchronizers_[syncTargetId];
if (!reg.db()) throw new Error('Cannot initialize synchronizer: db not initialized'); if (!reg.db()) throw new Error('Cannot initialize synchronizer: db not initialized');
@@ -191,6 +191,7 @@ reg.scheduleSync = async (delay = null) => {
reg.syncStarted = async () => { reg.syncStarted = async () => {
const syncTarget = Setting.value('sync.target'); const syncTarget = Setting.value('sync.target');
if (!reg.synchronizers_[syncTarget]) return false;
if (!reg.syncHasAuth(syncTarget)) return false; if (!reg.syncHasAuth(syncTarget)) return false;
const sync = await reg.synchronizer(syncTarget); const sync = await reg.synchronizer(syncTarget);
return sync.state() != 'idle'; return sync.state() != 'idle';