1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

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

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;

View File

@ -23,6 +23,7 @@ const RootWidget = require('tkwidgets/RootWidget.js');
const WindowWidget = require('tkwidgets/WindowWidget.js');
const NoteWidget = require('./gui/NoteWidget.js');
const ResourceServer = require('./ResourceServer.js');
const NoteMetadataWidget = require('./gui/NoteMetadataWidget.js');
const FolderListWidget = require('./gui/FolderListWidget.js');
const NoteListWidget = require('./gui/NoteListWidget.js');
@ -229,6 +230,10 @@ class AppGui {
this.rootWidget_.addChild(win1);
}
addCommandToConsole(cmd) {
this.stdout(chalk.cyan.bold('> ' + cmd));
}
setupShortcuts() {
const shortcuts = {};
@ -291,7 +296,7 @@ class AppGui {
action: async () => {
const cmd = await this.widget('statusBar').prompt();
if (!cmd) return;
this.stdout(chalk.cyan.bold('> ' + cmd));
this.addCommandToConsole(cmd);
await this.processCommand(cmd);
},
};
@ -327,17 +332,17 @@ class AppGui {
shortcuts['nn'] = {
description: _('Create a new note'),
action: { type: 'prompt', initialText: 'mknote ' },
action: { type: 'prompt', initialText: 'mknote ""', cursorPosition: -2 },
}
shortcuts['nt'] = {
description: _('Create a new todo'),
action: { type: 'prompt', initialText: 'mktodo ' },
action: { type: 'prompt', initialText: 'mktodo ""', cursorPosition: -2 },
}
shortcuts['nb'] = {
description: _('Create a new notebook'),
action: { type: 'prompt', initialText: 'mkbook ' },
action: { type: 'prompt', initialText: 'mkbook ""', cursorPosition: -2 },
}
return shortcuts;
@ -526,6 +531,11 @@ class AppGui {
this.updateStatusBarMessage();
}
exit() {
this.fullScreen(false);
this.resourceServer_.stop();
}
updateStatusBarMessage() {
const consoleWidget = this.widget('console');
@ -547,12 +557,58 @@ class AppGui {
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() {
const term = this.term();
this.fullScreen();
try {
this.setupResourceServer();
this.renderer_.start();
const statusBar = this.widget('statusBar');
@ -630,8 +686,10 @@ class AppGui {
await cmd.action();
} else if (typeof cmd.action === 'object') {
if (cmd.action.type === 'prompt') {
const commandString = await statusBar.prompt(cmd.action.initialText ? cmd.action.initialText : '');
this.stdout(commandString);
let promptOptions = {};
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);
} else {
throw new Error('Unknown command: ' + JSON.stringify(cmd.action));

View File

@ -315,9 +315,24 @@ class Application {
}
async exit(code = 0) {
await Setting.saveAll();
this.gui().fullScreen(false);
process.exit(code);
const doExit = async () => {
await Setting.saveAll();
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() {
@ -403,6 +418,7 @@ class Application {
maximizeConsole: () => {},
stdout: (text) => { console.info(text); },
fullScreen: (b=true) => {},
exit: () => {},
};
}

View File

@ -63,7 +63,7 @@ class Command extends BaseCommand {
await Resource.save(resource, { isNew: true });
note.body += "\n" + Resource.markdownTag(resource);
note.body += "\n\n" + Resource.markdownTag(resource);
await Note.save(note);
}

View File

@ -32,7 +32,7 @@ class FolderListWidget extends ListWidget {
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(' ');
};

View File

@ -27,8 +27,10 @@ class StatusBarWidget extends BaseWidget {
this.invalidate();
}
async prompt(initialText = '', promptString = ':') {
async prompt(initialText = '', promptString = null, options = null) {
if (this.promptState_) throw new Error('Another prompt already active');
if (promptString === null) promptString = ':';
if (options === null) options = {};
this.root.globalDisableKeyboard(this);
@ -38,6 +40,8 @@ class StatusBarWidget extends BaseWidget {
promptString: stripAnsi(promptString),
};
if ('cursorPosition' in options) this.promptState_.cursorPosition = options.cursorPosition;
this.promptState_.promise = new Promise((resolve, reject) => {
this.promptState_.resolve = resolve;
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
};
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
this.inputEventEmitter_ = this.term.inputField(options, (error, input) => {
let resolveResult = null;
const resolveFn = this.promptState_.resolve;

View File

@ -1,6 +1,6 @@
import { _ } from 'lib/locale.js'
import { netUtils } from 'lib/net-utils.js'
const tcpPortUsed = require('tcp-port-used');
const http = require("http");
const urlParser = require("url");
const FormData = require('form-data');
@ -43,16 +43,7 @@ class OneDriveApiNodeUtils {
this.api().setAuth(null);
let ports = 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;
}
}
const port = netUtils.findAvailablePort(this.possibleOAuthDancePorts());
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);

View File

@ -90,6 +90,9 @@ msgstr ""
msgid "Exits the application."
msgstr ""
msgid "Cancelling background synchronisation... Please wait."
msgstr ""
msgid "Only Bash is currently supported for autocompletion."
msgstr ""

View File

@ -100,6 +100,10 @@ msgstr "Affiche l'aide pour la commande donnée."
msgid "Exits the application."
msgstr "Quitter le logiciel."
#, fuzzy
msgid "Cancelling background synchronisation... Please wait."
msgstr "Annulation..."
msgid "Only Bash is currently supported for autocompletion."
msgstr ""

View File

@ -90,6 +90,9 @@ msgstr ""
msgid "Exits the application."
msgstr ""
msgid "Cancelling background synchronisation... Please wait."
msgstr ""
msgid "Only Bash is currently supported for autocompletion."
msgstr ""

View File

@ -14,8 +14,8 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1"
ajv@^5.1.0:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.4.tgz#3daf9a8b67221299fdae8d82d117ed8e6c80244b"
version "5.2.5"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.5.tgz#b637234d3e2675eb5f79fc652242a853a48cb49f"
dependencies:
co "^4.6.0"
fast-deep-equal "^1.0.0"
@ -832,8 +832,8 @@ chalk@^1.1.3:
supports-color "^2.0.0"
chalk@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.2.0.tgz#477b3bf2f9b8fd5ca9e429747e37f724ee7af240"
version "2.3.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
dependencies:
ansi-styles "^3.1.0"
escape-string-regexp "^1.0.5"
@ -1815,9 +1815,9 @@ node-bitmap@0.0.1:
version "0.0.1"
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"
resolved "https://github.com/laurent22/node-emoji#9fa01eac463e94dde1316ef8c53089eeef4973b5"
resolved "git+https://github.com/laurent22/node-emoji.git#9fa01eac463e94dde1316ef8c53089eeef4973b5"
dependencies:
lodash.toarray "^4.4.0"
@ -2455,8 +2455,8 @@ tcp-port-used@^0.1.2:
q "0.9.7"
terminal-kit@^1.13.11:
version "1.13.13"
resolved "https://registry.yarnpkg.com/terminal-kit/-/terminal-kit-1.13.13.tgz#fa53dbc17f5fb7e1630784274a7e57a10b1c0201"
version "1.14.0"
resolved "https://registry.yarnpkg.com/terminal-kit/-/terminal-kit-1.14.0.tgz#1cb89c22bf697152a3f8e54569c61d5084ed690a"
dependencies:
async-kit "^2.2.3"
get-pixels "^3.3.0"

View File

@ -109,6 +109,15 @@ class BaseModel {
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) {
return this.modelSelectAll('SELECT * FROM `' + this.tableName() + '` WHERE `id` LIKE ?', [partialId + '%']);
}

View File

@ -5,7 +5,7 @@ import { mime } from 'lib/mime-utils.js';
import { filename } from 'lib/path-utils.js';
import { FsDriverDummy } from 'lib/fs-driver-dummy.js';
import { markdownUtils } from 'lib/markdown-utils.js';
import lodash from 'lodash';
import lodash from 'lodash';
class Resource extends BaseItem {
@ -59,7 +59,7 @@ class Resource extends BaseItem {
return filename(path);
}
static content(resource) {
static async content(resource) {
return this.fsDriver().readFile(this.fullPath(resource));
}

View File

@ -1,5 +1,7 @@
import { shim } from 'lib/shim.js'
const tcpPortUsed = require('tcp-port-used');
const netUtils = {};
netUtils.ip = async () => {
@ -12,4 +14,20 @@ netUtils.ip = async () => {
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 };

View File

@ -14,6 +14,7 @@ import { _ } from 'lib/locale.js';
const reg = {};
reg.initSynchronizerStates_ = {};
reg.synchronizers_ = {};
reg.logger = () => {
if (!reg.logger_) {
@ -96,7 +97,6 @@ reg.initSynchronizer_ = async (syncTargetId) => {
}
reg.synchronizer = async (syncTargetId) => {
if (!reg.synchronizers_) reg.synchronizers_ = [];
if (reg.synchronizers_[syncTargetId]) return reg.synchronizers_[syncTargetId];
if (!reg.db()) throw new Error('Cannot initialize synchronizer: db not initialized');
@ -191,6 +191,7 @@ reg.scheduleSync = async (delay = null) => {
reg.syncStarted = async () => {
const syncTarget = Setting.value('sync.target');
if (!reg.synchronizers_[syncTarget]) return false;
if (!reg.syncHasAuth(syncTarget)) return false;
const sync = await reg.synchronizer(syncTarget);
return sync.state() != 'idle';