mirror of
https://github.com/zadam/trilium.git
synced 2025-11-03 20:06:08 +01:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
780f462e94 | ||
|
|
488e657cc4 | ||
|
|
8bc2a21d80 | ||
|
|
743d72a0c3 | ||
|
|
20b1357be6 | ||
|
|
d9f2bb37e7 | ||
|
|
97c1b3061f | ||
|
|
c022fcf196 | ||
|
|
b5baab056c | ||
|
|
edc9a1a2bf | ||
|
|
c0e45a73a8 | ||
|
|
784cd62df1 | ||
|
|
91cf090820 | ||
|
|
d9f29cbf27 | ||
|
|
23a5e38e02 | ||
|
|
663bd1a8fe | ||
|
|
a6a687c4a6 | ||
|
|
f2aaf8b0a3 | ||
|
|
01ede22504 | ||
|
|
b6d617aefa | ||
|
|
7921850186 | ||
|
|
244a4562b1 | ||
|
|
07c33979c3 | ||
|
|
353a9b24c1 | ||
|
|
548ecd4171 |
@@ -67,10 +67,7 @@ CREATE INDEX `IDX_sync_sync_date` ON `sync` (
|
|||||||
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
||||||
`is_deleted`
|
`is_deleted`
|
||||||
);
|
);
|
||||||
CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` (
|
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
`note_tree_id`
|
|
||||||
);
|
|
||||||
CREATE UNIQUE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
|
||||||
`note_id`,
|
`note_id`,
|
||||||
`parent_note_id`
|
`parent_note_id`
|
||||||
);
|
);
|
||||||
|
|||||||
9
migrations/0062__change_index_back_to_non_unique.sql
Normal file
9
migrations/0062__change_index_back_to_non_unique.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
DROP INDEX IDX_notes_tree_note_id_parent_note_id;
|
||||||
|
|
||||||
|
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||||
|
`note_id`,
|
||||||
|
`parent_note_id`
|
||||||
|
);
|
||||||
|
|
||||||
|
-- dropping this as it's just duplicate of primary key
|
||||||
|
DROP INDEX IDX_notes_tree_note_tree_id;
|
||||||
11
migrations/0063__image_table.sql
Normal file
11
migrations/0063__image_table.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE images
|
||||||
|
(
|
||||||
|
image_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
format TEXT NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
data BLOB,
|
||||||
|
is_deleted INT NOT NULL DEFAULT 0,
|
||||||
|
date_modified TEXT NOT NULL,
|
||||||
|
date_created TEXT NOT NULL
|
||||||
|
);
|
||||||
16
migrations/0064__add_note_id_to_image_table.sql
Normal file
16
migrations/0064__add_note_id_to_image_table.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
DROP TABLE images;
|
||||||
|
|
||||||
|
CREATE TABLE images
|
||||||
|
(
|
||||||
|
image_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
note_id TEXT NOT NULL,
|
||||||
|
format TEXT NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
data BLOB,
|
||||||
|
is_deleted INT NOT NULL DEFAULT 0,
|
||||||
|
date_modified TEXT NOT NULL,
|
||||||
|
date_created TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX images_note_id_index ON images (note_id);
|
||||||
27
migrations/0065__notes_image.sql
Normal file
27
migrations/0065__notes_image.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
DROP TABLE images;
|
||||||
|
|
||||||
|
CREATE TABLE images
|
||||||
|
(
|
||||||
|
image_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
format TEXT NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
data BLOB,
|
||||||
|
is_deleted INT NOT NULL DEFAULT 0,
|
||||||
|
date_modified TEXT NOT NULL,
|
||||||
|
date_created TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE notes_image
|
||||||
|
(
|
||||||
|
note_image_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
note_id TEXT NOT NULL,
|
||||||
|
image_id TEXT NOT NULL,
|
||||||
|
is_deleted INT NOT NULL DEFAULT 0,
|
||||||
|
date_modified TEXT NOT NULL,
|
||||||
|
date_created TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX notes_image_note_id_index ON notes_image (note_id);
|
||||||
|
CREATE INDEX notes_image_image_id_index ON notes_image (image_id);
|
||||||
|
CREATE INDEX notes_image_note_id_image_id_index ON notes_image (note_id, image_id);
|
||||||
2318
package-lock.json
generated
2318
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.2.1",
|
"version": "0.3.0",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/zadam/trilium.git"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www",
|
"start": "node ./bin/www",
|
||||||
"test-electron": "xo",
|
"test-electron": "xo",
|
||||||
@@ -14,6 +19,7 @@
|
|||||||
"publish-forge": "electron-forge publish"
|
"publish-forge": "electron-forge publish"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"async-mutex": "^0.1.3",
|
||||||
"body-parser": "~1.18.2",
|
"body-parser": "~1.18.2",
|
||||||
"cookie-parser": "~1.4.3",
|
"cookie-parser": "~1.4.3",
|
||||||
"debug": "~3.1.0",
|
"debug": "~3.1.0",
|
||||||
@@ -23,15 +29,24 @@
|
|||||||
"electron-debug": "^1.0.0",
|
"electron-debug": "^1.0.0",
|
||||||
"electron-in-page-search": "^1.2.4",
|
"electron-in-page-search": "^1.2.4",
|
||||||
"express": "~4.16.2",
|
"express": "~4.16.2",
|
||||||
|
"express-promise-wrap": "^0.2.2",
|
||||||
"express-session": "^1.15.6",
|
"express-session": "^1.15.6",
|
||||||
"fs-extra": "^4.0.2",
|
"fs-extra": "^4.0.2",
|
||||||
"helmet": "^3.9.0",
|
"helmet": "^3.9.0",
|
||||||
"html": "^1.0.0",
|
"html": "^1.0.0",
|
||||||
|
"image-type": "^3.0.0",
|
||||||
|
"imagemin": "^5.3.1",
|
||||||
|
"imagemin-giflossy": "^5.1.10",
|
||||||
|
"imagemin-mozjpeg": "^7.0.0",
|
||||||
|
"imagemin-pngquant": "^5.0.1",
|
||||||
"ini": "^1.3.4",
|
"ini": "^1.3.4",
|
||||||
|
"jimp": "^0.2.28",
|
||||||
|
"multer": "^1.3.0",
|
||||||
"rand-token": "^0.4.0",
|
"rand-token": "^0.4.0",
|
||||||
"request": "^2.83.0",
|
"request": "^2.83.0",
|
||||||
"request-promise": "^4.2.2",
|
"request-promise": "^4.2.2",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
|
"sanitize-filename": "^1.6.1",
|
||||||
"scrypt": "^6.0.3",
|
"scrypt": "^6.0.3",
|
||||||
"serve-favicon": "~2.4.5",
|
"serve-favicon": "~2.4.5",
|
||||||
"session-file-store": "^1.1.2",
|
"session-file-store": "^1.1.2",
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ $(document).bind('keydown', "ctrl+shift+down", () => {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#note-title").bind('keydown', 'return', () => $("#note-detail").focus());
|
||||||
|
|
||||||
$(window).on('beforeunload', () => {
|
$(window).on('beforeunload', () => {
|
||||||
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
|
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
|
||||||
// this sends the request asynchronously and doesn't wait for result
|
// this sends the request asynchronously and doesn't wait for result
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ const messaging = (function() {
|
|||||||
|
|
||||||
const syncData = message.data.filter(sync => sync.source_id !== glob.sourceId);
|
const syncData = message.data.filter(sync => sync.source_id !== glob.sourceId);
|
||||||
|
|
||||||
if (syncData.some(sync => sync.entity_name === 'notes_tree')) {
|
if (syncData.some(sync => sync.entity_name === 'notes_tree')
|
||||||
|
|| syncData.some(sync => sync.entity_name === 'notes')) {
|
||||||
|
|
||||||
console.log(now(), "Reloading tree because of background changes");
|
console.log(now(), "Reloading tree because of background changes");
|
||||||
|
|
||||||
noteTree.reload();
|
noteTree.reload();
|
||||||
@@ -47,6 +49,9 @@ const messaging = (function() {
|
|||||||
recentNotes.reload();
|
recentNotes.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we don't detect image changes here since images themselves are immutable and references should be
|
||||||
|
// updated in note detail as well
|
||||||
|
|
||||||
changesToPushCountEl.html(message.changesToPushCount);
|
changesToPushCountEl.html(message.changesToPushCount);
|
||||||
}
|
}
|
||||||
else if (message.type === 'sync-hash-check-failed') {
|
else if (message.type === 'sync-hash-check-failed') {
|
||||||
|
|||||||
@@ -490,12 +490,14 @@ const noteTree = (function() {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
"ctrl+return": node => {
|
|
||||||
noteDetailEl.focus();
|
|
||||||
},
|
|
||||||
"return": node => {
|
"return": node => {
|
||||||
noteDetailEl.focus();
|
noteDetailEl.focus();
|
||||||
},
|
},
|
||||||
|
"backspace": node => {
|
||||||
|
if (!isTopLevelNode(node)) {
|
||||||
|
node.getParent().setActive().then(() => clearSelectedNodes());
|
||||||
|
}
|
||||||
|
},
|
||||||
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
|
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
|
||||||
// after opening context menu, standard shortcuts don't work, but they are detected here
|
// after opening context menu, standard shortcuts don't work, but they are detected here
|
||||||
// so we essentially takeover the standard handling with our implementation.
|
// so we essentially takeover the standard handling with our implementation.
|
||||||
@@ -532,13 +534,15 @@ const noteTree = (function() {
|
|||||||
const node = data.node;
|
const node = data.node;
|
||||||
|
|
||||||
if (targetType === 'title' || targetType === 'icon') {
|
if (targetType === 'title' || targetType === 'icon') {
|
||||||
node.setActive();
|
|
||||||
|
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
|
node.setActive();
|
||||||
|
node.setSelected(true);
|
||||||
|
|
||||||
clearSelectedNodes();
|
clearSelectedNodes();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
node.setSelected(true);
|
node.setSelected(!node.isSelected());
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const server = (function() {
|
|||||||
get,
|
get,
|
||||||
post,
|
post,
|
||||||
put,
|
put,
|
||||||
remove
|
remove,
|
||||||
|
getHeaders
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
4
public/libraries/ckeditor/ckeditor.js
vendored
4
public/libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
631
public/libraries/jquery.ui-contextmenu.js
Normal file
631
public/libraries/jquery.ui-contextmenu.js
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* jquery.ui-contextmenu.js plugin.
|
||||||
|
*
|
||||||
|
* jQuery plugin that provides a context menu (based on the jQueryUI menu widget).
|
||||||
|
*
|
||||||
|
* @see https://github.com/mar10/jquery-ui-contextmenu
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013-2017, Martin Wendt (http://wwWendt.de). Licensed MIT.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function( factory ) {
|
||||||
|
"use strict";
|
||||||
|
if ( typeof define === "function" && define.amd ) {
|
||||||
|
// AMD. Register as an anonymous module.
|
||||||
|
define([ "jquery", "jquery-ui/ui/widgets/menu" ], factory );
|
||||||
|
} else {
|
||||||
|
// Browser globals
|
||||||
|
factory( jQuery );
|
||||||
|
}
|
||||||
|
}(function( $ ) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var supportSelectstart = "onselectstart" in document.createElement("div"),
|
||||||
|
match = $.ui.menu.version.match(/^(\d)\.(\d+)/),
|
||||||
|
uiVersion = {
|
||||||
|
major: parseInt(match[1], 10),
|
||||||
|
minor: parseInt(match[2], 10)
|
||||||
|
},
|
||||||
|
isLTE110 = ( uiVersion.major < 2 && uiVersion.minor <= 10 ),
|
||||||
|
isLTE111 = ( uiVersion.major < 2 && uiVersion.minor <= 11 );
|
||||||
|
|
||||||
|
$.widget("moogle.contextmenu", {
|
||||||
|
version: "@VERSION",
|
||||||
|
options: {
|
||||||
|
addClass: "ui-contextmenu", // Add this class to the outer <ul>
|
||||||
|
closeOnWindowBlur: true, // Close menu when window loses focus
|
||||||
|
autoFocus: false, // Set keyboard focus to first entry on open
|
||||||
|
autoTrigger: true, // open menu on browser's `contextmenu` event
|
||||||
|
delegate: null, // selector
|
||||||
|
hide: { effect: "fadeOut", duration: "fast" },
|
||||||
|
ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents
|
||||||
|
menu: null, // selector or jQuery pointing to <UL>, or a definition hash
|
||||||
|
position: null, // popup positon
|
||||||
|
preventContextMenuForPopup: false, // prevent opening the browser's system
|
||||||
|
// context menu on menu entries
|
||||||
|
preventSelect: false, // disable text selection of target
|
||||||
|
show: { effect: "slideDown", duration: "fast" },
|
||||||
|
taphold: false, // open menu on taphold events (requires external plugins)
|
||||||
|
uiMenuOptions: {}, // Additional options, used when UI Menu is created
|
||||||
|
// Events:
|
||||||
|
beforeOpen: $.noop, // menu about to open; return `false` to prevent opening
|
||||||
|
blur: $.noop, // menu option lost focus
|
||||||
|
close: $.noop, // menu was closed
|
||||||
|
create: $.noop, // menu was initialized
|
||||||
|
createMenu: $.noop, // menu was initialized (original UI Menu)
|
||||||
|
focus: $.noop, // menu option got focus
|
||||||
|
open: $.noop, // menu was opened
|
||||||
|
select: $.noop // menu option was selected; return `false` to prevent closing
|
||||||
|
},
|
||||||
|
/** Constructor */
|
||||||
|
_create: function() {
|
||||||
|
var cssText, eventNames, targetId,
|
||||||
|
opts = this.options;
|
||||||
|
|
||||||
|
this.$headStyle = null;
|
||||||
|
this.$menu = null;
|
||||||
|
this.menuIsTemp = false;
|
||||||
|
this.currentTarget = null;
|
||||||
|
this.extraData = {};
|
||||||
|
this.previousFocus = null;
|
||||||
|
|
||||||
|
if (opts.delegate == null) {
|
||||||
|
$.error("ui-contextmenu: Missing required option `delegate`.");
|
||||||
|
}
|
||||||
|
if (opts.preventSelect) {
|
||||||
|
// Create a global style for all potential menu targets
|
||||||
|
// If the contextmenu was bound to `document`, we apply the
|
||||||
|
// selector relative to the <body> tag instead
|
||||||
|
targetId = ($(this.element).is(document) ? $("body")
|
||||||
|
: this.element).uniqueId().attr("id");
|
||||||
|
cssText = "#" + targetId + " " + opts.delegate + " { " +
|
||||||
|
"-webkit-user-select: none; " +
|
||||||
|
"-khtml-user-select: none; " +
|
||||||
|
"-moz-user-select: none; " +
|
||||||
|
"-ms-user-select: none; " +
|
||||||
|
"user-select: none; " +
|
||||||
|
"}";
|
||||||
|
this.$headStyle = $("<style class='moogle-contextmenu-style' />")
|
||||||
|
.prop("type", "text/css")
|
||||||
|
.appendTo("head");
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.$headStyle.html(cssText);
|
||||||
|
} catch ( e ) {
|
||||||
|
// issue #47: fix for IE 6-8
|
||||||
|
this.$headStyle[0].styleSheet.cssText = cssText;
|
||||||
|
}
|
||||||
|
// TODO: the selectstart is not supported by FF?
|
||||||
|
if (supportSelectstart) {
|
||||||
|
this.element.on("selectstart" + this.eventNamespace, opts.delegate,
|
||||||
|
function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._createUiMenu(opts.menu);
|
||||||
|
|
||||||
|
eventNames = "contextmenu" + this.eventNamespace;
|
||||||
|
if (opts.taphold) {
|
||||||
|
eventNames += " taphold" + this.eventNamespace;
|
||||||
|
}
|
||||||
|
this.element.on(eventNames, opts.delegate, $.proxy(this._openMenu, this));
|
||||||
|
},
|
||||||
|
/** Destructor, called on $().contextmenu("destroy"). */
|
||||||
|
_destroy: function() {
|
||||||
|
this.element.off(this.eventNamespace);
|
||||||
|
|
||||||
|
this._createUiMenu(null);
|
||||||
|
|
||||||
|
if (this.$headStyle) {
|
||||||
|
this.$headStyle.remove();
|
||||||
|
this.$headStyle = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** (Re)Create jQuery UI Menu. */
|
||||||
|
_createUiMenu: function(menuDef) {
|
||||||
|
var ct, ed,
|
||||||
|
opts = this.options;
|
||||||
|
|
||||||
|
// Remove temporary <ul> if any
|
||||||
|
if (this.isOpen()) {
|
||||||
|
// #58: 'replaceMenu' in beforeOpen causing select: to lose ui.target
|
||||||
|
ct = this.currentTarget;
|
||||||
|
ed = this.extraData;
|
||||||
|
// close without animation, to force async mode
|
||||||
|
this._closeMenu(true);
|
||||||
|
this.currentTarget = ct;
|
||||||
|
this.extraData = ed;
|
||||||
|
}
|
||||||
|
if (this.menuIsTemp) {
|
||||||
|
this.$menu.remove(); // this will also destroy ui.menu
|
||||||
|
} else if (this.$menu) {
|
||||||
|
this.$menu
|
||||||
|
.menu("destroy")
|
||||||
|
.removeClass(this.options.addClass)
|
||||||
|
.hide();
|
||||||
|
}
|
||||||
|
this.$menu = null;
|
||||||
|
this.menuIsTemp = false;
|
||||||
|
// If a menu definition array was passed, create a hidden <ul>
|
||||||
|
// and generate the structure now
|
||||||
|
if ( !menuDef ) {
|
||||||
|
return;
|
||||||
|
} else if ($.isArray(menuDef)) {
|
||||||
|
this.$menu = $.moogle.contextmenu.createMenuMarkup(menuDef);
|
||||||
|
this.menuIsTemp = true;
|
||||||
|
}else if ( typeof menuDef === "string" ) {
|
||||||
|
this.$menu = $(menuDef);
|
||||||
|
} else {
|
||||||
|
this.$menu = menuDef;
|
||||||
|
}
|
||||||
|
// Create - but hide - the jQuery UI Menu widget
|
||||||
|
this.$menu
|
||||||
|
.hide()
|
||||||
|
.addClass(opts.addClass)
|
||||||
|
// Create a menu instance that delegates events to our widget
|
||||||
|
.menu($.extend(true, {}, opts.uiMenuOptions, {
|
||||||
|
items: "> :not(.ui-widget-header)",
|
||||||
|
blur: $.proxy(opts.blur, this),
|
||||||
|
create: $.proxy(opts.createMenu, this),
|
||||||
|
focus: $.proxy(opts.focus, this),
|
||||||
|
select: $.proxy(function(event, ui) {
|
||||||
|
// User selected a menu entry
|
||||||
|
var retval,
|
||||||
|
isParent = $.moogle.contextmenu.isMenu(ui.item),
|
||||||
|
actionHandler = ui.item.data("actionHandler");
|
||||||
|
|
||||||
|
ui.cmd = ui.item.attr("data-command");
|
||||||
|
ui.target = $(this.currentTarget);
|
||||||
|
ui.extraData = this.extraData;
|
||||||
|
// ignore clicks, if they only open a sub-menu
|
||||||
|
if ( !isParent || !opts.ignoreParentSelect) {
|
||||||
|
retval = this._trigger.call(this, "select", event, ui);
|
||||||
|
if ( actionHandler ) {
|
||||||
|
retval = actionHandler.call(this, event, ui);
|
||||||
|
}
|
||||||
|
if ( retval !== false ) {
|
||||||
|
this._closeMenu.call(this);
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, this)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
/** Open popup (called on 'contextmenu' event). */
|
||||||
|
_openMenu: function(event, recursive) {
|
||||||
|
var res, promise, ui,
|
||||||
|
opts = this.options,
|
||||||
|
posOption = opts.position,
|
||||||
|
self = this,
|
||||||
|
manualTrigger = !!event.isTrigger;
|
||||||
|
|
||||||
|
if ( !opts.autoTrigger && !manualTrigger ) {
|
||||||
|
// ignore browser's `contextmenu` events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prevent browser from opening the system context menu
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.currentTarget = event.target;
|
||||||
|
this.extraData = event._extraData || {};
|
||||||
|
|
||||||
|
ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData,
|
||||||
|
originalEvent: event, result: null };
|
||||||
|
|
||||||
|
if ( !recursive ) {
|
||||||
|
res = this._trigger("beforeOpen", event, ui);
|
||||||
|
promise = (ui.result && $.isFunction(ui.result.promise)) ? ui.result : null;
|
||||||
|
ui.result = null;
|
||||||
|
if ( res === false ) {
|
||||||
|
this.currentTarget = null;
|
||||||
|
return false;
|
||||||
|
} else if ( promise ) {
|
||||||
|
// Handler returned a Deferred or Promise. Delay menu open until
|
||||||
|
// the promise is resolved
|
||||||
|
promise.done(function() {
|
||||||
|
self._openMenu(event, true);
|
||||||
|
});
|
||||||
|
this.currentTarget = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ui.menu = this.$menu; // Might have changed in beforeOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register global event handlers that close the dropdown-menu
|
||||||
|
$(document).on("keydown" + this.eventNamespace, function(event) {
|
||||||
|
if ( event.which === $.ui.keyCode.ESCAPE ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
}).on("mousedown" + this.eventNamespace + " touchstart" + this.eventNamespace,
|
||||||
|
function(event) {
|
||||||
|
// Close menu when clicked outside menu
|
||||||
|
if ( !$(event.target).closest(".ui-menu-item").length ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(window).on("blur" + this.eventNamespace, function(event) {
|
||||||
|
if ( opts.closeOnWindowBlur ) {
|
||||||
|
self._closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// required for custom positioning (issue #18 and #13).
|
||||||
|
if ($.isFunction(posOption)) {
|
||||||
|
posOption = posOption(event, ui);
|
||||||
|
}
|
||||||
|
posOption = $.extend({
|
||||||
|
my: "left top",
|
||||||
|
at: "left bottom",
|
||||||
|
// if called by 'open' method, event does not have pageX/Y
|
||||||
|
of: (event.pageX === undefined) ? event.target : event,
|
||||||
|
collision: "fit"
|
||||||
|
}, posOption);
|
||||||
|
|
||||||
|
// Update entry statuses from callbacks
|
||||||
|
this._updateEntries(this.$menu);
|
||||||
|
|
||||||
|
// Finally display the popup
|
||||||
|
this.$menu
|
||||||
|
.show() // required to fix positioning error
|
||||||
|
.css({
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0
|
||||||
|
}).position(posOption)
|
||||||
|
.hide(); // hide again, so we can apply nice effects
|
||||||
|
|
||||||
|
if ( opts.preventContextMenuForPopup ) {
|
||||||
|
this.$menu.on("contextmenu" + this.eventNamespace, function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._show(this.$menu, opts.show, function() {
|
||||||
|
var $first;
|
||||||
|
|
||||||
|
// Set focus to first active menu entry
|
||||||
|
if ( opts.autoFocus ) {
|
||||||
|
self.previousFocus = $(event.target);
|
||||||
|
// self.$menu.focus();
|
||||||
|
$first = self.$menu
|
||||||
|
.children("li.ui-menu-item")
|
||||||
|
.not(".ui-state-disabled")
|
||||||
|
.first();
|
||||||
|
self.$menu.menu("focus", null, $first).focus();
|
||||||
|
}
|
||||||
|
self._trigger.call(self, "open", event, ui);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** Close popup. */
|
||||||
|
_closeMenu: function(immediately) {
|
||||||
|
var self = this,
|
||||||
|
hideOpts = immediately ? false : this.options.hide,
|
||||||
|
ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
|
||||||
|
|
||||||
|
// Note: we don't want to unbind the 'contextmenu' event
|
||||||
|
$(document)
|
||||||
|
.off("mousedown" + this.eventNamespace)
|
||||||
|
.off("touchstart" + this.eventNamespace)
|
||||||
|
.off("keydown" + this.eventNamespace);
|
||||||
|
$(window)
|
||||||
|
.off("blur" + this.eventNamespace);
|
||||||
|
|
||||||
|
self.currentTarget = null; // issue #44 after hide animation is too late
|
||||||
|
self.extraData = {};
|
||||||
|
if ( this.$menu ) { // #88: widget might have been destroyed already
|
||||||
|
this.$menu
|
||||||
|
.off("contextmenu" + this.eventNamespace);
|
||||||
|
this._hide(this.$menu, hideOpts, function() {
|
||||||
|
if ( self.previousFocus ) {
|
||||||
|
self.previousFocus.focus();
|
||||||
|
self.previousFocus = null;
|
||||||
|
}
|
||||||
|
self._trigger("close", null, ui);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self._trigger("close", null, ui);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Handle $().contextmenu("option", key, value) calls. */
|
||||||
|
_setOption: function(key, value) {
|
||||||
|
switch (key) {
|
||||||
|
case "menu":
|
||||||
|
this.replaceMenu(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$.Widget.prototype._setOption.apply(this, arguments);
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry (<LI> tag). */
|
||||||
|
_getMenuEntry: function(cmd) {
|
||||||
|
return this.$menu.find("li[data-command=" + cmd + "]");
|
||||||
|
},
|
||||||
|
/** Close context menu. */
|
||||||
|
close: function() {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
this._closeMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* Apply status callbacks when menu is opened. */
|
||||||
|
_updateEntries: function() {
|
||||||
|
var self = this,
|
||||||
|
ui = {
|
||||||
|
menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
|
||||||
|
|
||||||
|
$.each(this.$menu.find(".ui-menu-item"), function(i, o) {
|
||||||
|
var $entry = $(o),
|
||||||
|
fn = $entry.data("disabledHandler"),
|
||||||
|
res = fn ? fn({ type: "disabled" }, ui) : null;
|
||||||
|
|
||||||
|
ui.item = $entry;
|
||||||
|
ui.cmd = $entry.attr("data-command");
|
||||||
|
// Evaluate `disabled()` callback
|
||||||
|
if ( res != null ) {
|
||||||
|
self.enableEntry(ui.cmd, !res);
|
||||||
|
self.showEntry(ui.cmd, res !== "hide");
|
||||||
|
}
|
||||||
|
// Evaluate `title()` callback
|
||||||
|
fn = $entry.data("titleHandler"),
|
||||||
|
res = fn ? fn({ type: "title" }, ui) : null;
|
||||||
|
if ( res != null ) {
|
||||||
|
self.setTitle(ui.cmd, "" + res);
|
||||||
|
}
|
||||||
|
// Evaluate `tooltip()` callback
|
||||||
|
fn = $entry.data("tooltipHandler"),
|
||||||
|
res = fn ? fn({ type: "tooltip" }, ui) : null;
|
||||||
|
if ( res != null ) {
|
||||||
|
$entry.attr("title", "" + res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** Enable or disable the menu command. */
|
||||||
|
enableEntry: function(cmd, flag) {
|
||||||
|
this._getMenuEntry(cmd).toggleClass("ui-state-disabled", (flag === false));
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry (LI tag) as jQuery object. */
|
||||||
|
getEntry: function(cmd) {
|
||||||
|
return this._getMenuEntry(cmd);
|
||||||
|
},
|
||||||
|
/** Return ui-menu entry wrapper as jQuery object.
|
||||||
|
UI 1.10: this is the <a> tag inside the LI
|
||||||
|
UI 1.11: this is the LI istself
|
||||||
|
UI 1.12: this is the <div> tag inside the LI
|
||||||
|
*/
|
||||||
|
getEntryWrapper: function(cmd) {
|
||||||
|
return this._getMenuEntry(cmd).find(">[role=menuitem]").addBack("[role=menuitem]");
|
||||||
|
},
|
||||||
|
/** Return Menu element (UL). */
|
||||||
|
getMenu: function() {
|
||||||
|
return this.$menu;
|
||||||
|
},
|
||||||
|
/** Return true if menu is open. */
|
||||||
|
isOpen: function() {
|
||||||
|
// return this.$menu && this.$menu.is(":visible");
|
||||||
|
return !!this.$menu && !!this.currentTarget;
|
||||||
|
},
|
||||||
|
/** Open context menu on a specific target (must match options.delegate)
|
||||||
|
* Optional `extraData` is passed to event handlers as `ui.extraData`.
|
||||||
|
*/
|
||||||
|
open: function(targetOrEvent, extraData) {
|
||||||
|
// Fake a 'contextmenu' event
|
||||||
|
extraData = extraData || {};
|
||||||
|
|
||||||
|
var isEvent = (targetOrEvent && targetOrEvent.type && targetOrEvent.target),
|
||||||
|
event = isEvent ? targetOrEvent : {},
|
||||||
|
target = isEvent ? targetOrEvent.target : targetOrEvent,
|
||||||
|
e = jQuery.Event("contextmenu", {
|
||||||
|
target: $(target).get(0),
|
||||||
|
pageX: event.pageX,
|
||||||
|
pageY: event.pageY,
|
||||||
|
originalEvent: isEvent ? targetOrEvent : undefined,
|
||||||
|
_extraData: extraData
|
||||||
|
});
|
||||||
|
return this.element.trigger(e);
|
||||||
|
},
|
||||||
|
/** Replace the menu altogether. */
|
||||||
|
replaceMenu: function(data) {
|
||||||
|
this._createUiMenu(data);
|
||||||
|
},
|
||||||
|
/** Redefine a whole menu entry. */
|
||||||
|
setEntry: function(cmd, entry) {
|
||||||
|
var $ul,
|
||||||
|
$entryLi = this._getMenuEntry(cmd);
|
||||||
|
|
||||||
|
if (typeof entry === "string") {
|
||||||
|
window.console && window.console.warn(
|
||||||
|
"setEntry(cmd, t) with a plain string title is deprecated since v1.18." +
|
||||||
|
"Use setTitle(cmd, '" + entry + "') instead.");
|
||||||
|
return this.setTitle(cmd, entry);
|
||||||
|
}
|
||||||
|
$entryLi.empty();
|
||||||
|
entry.cmd = entry.cmd || cmd;
|
||||||
|
$.moogle.contextmenu.createEntryMarkup(entry, $entryLi);
|
||||||
|
if ($.isArray(entry.children)) {
|
||||||
|
$ul = $("<ul/>").appendTo($entryLi);
|
||||||
|
$.moogle.contextmenu.createMenuMarkup(entry.children, $ul);
|
||||||
|
}
|
||||||
|
// #110: jQuery UI 1.12: refresh only works when this class is not set:
|
||||||
|
$entryLi.removeClass("ui-menu-item");
|
||||||
|
this.getMenu().menu("refresh");
|
||||||
|
},
|
||||||
|
/** Set icon (pass null to remove). */
|
||||||
|
setIcon: function(cmd, icon) {
|
||||||
|
return this.updateEntry(cmd, { uiIcon: icon });
|
||||||
|
},
|
||||||
|
/** Set title. */
|
||||||
|
setTitle: function(cmd, title) {
|
||||||
|
return this.updateEntry(cmd, { title: title });
|
||||||
|
},
|
||||||
|
// /** Set tooltip (pass null to remove). */
|
||||||
|
// setTooltip: function(cmd, tooltip) {
|
||||||
|
// this._getMenuEntry(cmd).attr("title", tooltip);
|
||||||
|
// },
|
||||||
|
/** Show or hide the menu command. */
|
||||||
|
showEntry: function(cmd, flag) {
|
||||||
|
this._getMenuEntry(cmd).toggle(flag !== false);
|
||||||
|
},
|
||||||
|
/** Redefine selective attributes of a menu entry. */
|
||||||
|
updateEntry: function(cmd, entry) {
|
||||||
|
var $icon, $wrapper,
|
||||||
|
$entryLi = this._getMenuEntry(cmd);
|
||||||
|
|
||||||
|
if ( entry.title !== undefined ) {
|
||||||
|
$.moogle.contextmenu.updateTitle($entryLi, "" + entry.title);
|
||||||
|
}
|
||||||
|
if ( entry.tooltip !== undefined ) {
|
||||||
|
if ( entry.tooltip === null ) {
|
||||||
|
$entryLi.removeAttr("title");
|
||||||
|
} else {
|
||||||
|
$entryLi.attr("title", entry.tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( entry.uiIcon !== undefined ) {
|
||||||
|
$wrapper = this.getEntryWrapper(cmd),
|
||||||
|
$icon = $wrapper.find("span.ui-icon").not(".ui-menu-icon");
|
||||||
|
$icon.remove();
|
||||||
|
if ( entry.uiIcon ) {
|
||||||
|
$wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( entry.hide !== undefined ) {
|
||||||
|
$entryLi.toggle(!entry.hide);
|
||||||
|
} else if ( entry.show !== undefined ) {
|
||||||
|
// Note: `show` is an undocumented variant. `hide: false` is preferred
|
||||||
|
$entryLi.toggle(!!entry.show);
|
||||||
|
}
|
||||||
|
// if ( entry.isHeader !== undefined ) {
|
||||||
|
// $entryLi.toggleClass("ui-widget-header", !!entry.isHeader);
|
||||||
|
// }
|
||||||
|
if ( entry.data !== undefined ) {
|
||||||
|
$entryLi.data(entry.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set/clear class names, but handle ui-state-disabled separately
|
||||||
|
if ( entry.disabled === undefined ) {
|
||||||
|
entry.disabled = $entryLi.hasClass("ui-state-disabled");
|
||||||
|
}
|
||||||
|
if ( entry.setClass ) {
|
||||||
|
if ( $entryLi.hasClass("ui-menu-item") ) {
|
||||||
|
entry.setClass += " ui-menu-item";
|
||||||
|
}
|
||||||
|
$entryLi.removeClass();
|
||||||
|
$entryLi.addClass(entry.setClass);
|
||||||
|
} else if ( entry.addClass ) {
|
||||||
|
$entryLi.addClass(entry.addClass);
|
||||||
|
}
|
||||||
|
$entryLi.toggleClass("ui-state-disabled", !!entry.disabled);
|
||||||
|
// // #110: jQuery UI 1.12: refresh only works when this class is not set:
|
||||||
|
// $entryLi.removeClass("ui-menu-item");
|
||||||
|
// this.getMenu().menu("refresh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global functions
|
||||||
|
*/
|
||||||
|
$.extend($.moogle.contextmenu, {
|
||||||
|
/** Convert a menu description into a into a <li> content. */
|
||||||
|
createEntryMarkup: function(entry, $parentLi) {
|
||||||
|
var $wrapper = null;
|
||||||
|
|
||||||
|
$parentLi.attr("data-command", entry.cmd);
|
||||||
|
|
||||||
|
if ( !/[^\-\u2014\u2013\s]/.test( entry.title ) ) {
|
||||||
|
// hyphen, em dash, en dash: separator as defined by UI Menu 1.10
|
||||||
|
$parentLi.text(entry.title);
|
||||||
|
} else {
|
||||||
|
if ( isLTE110 ) {
|
||||||
|
// jQuery UI Menu 1.10 or before required an `<a>` tag
|
||||||
|
$wrapper = $("<a/>", {
|
||||||
|
html: "" + entry.title,
|
||||||
|
href: "#"
|
||||||
|
}).appendTo($parentLi);
|
||||||
|
|
||||||
|
} else if ( isLTE111 ) {
|
||||||
|
// jQuery UI Menu 1.11 preferes to avoid `<a>` tags or <div> wrapper
|
||||||
|
$parentLi.html("" + entry.title);
|
||||||
|
$wrapper = $parentLi;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// jQuery UI Menu 1.12 introduced `<div>` wrappers
|
||||||
|
$wrapper = $("<div/>", {
|
||||||
|
html: "" + entry.title
|
||||||
|
}).appendTo($parentLi);
|
||||||
|
}
|
||||||
|
if ( entry.uiIcon ) {
|
||||||
|
$wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
|
||||||
|
}
|
||||||
|
// Store option callbacks in entry's data
|
||||||
|
$.each( [ "action", "disabled", "title", "tooltip" ], function(i, attr) {
|
||||||
|
if ( $.isFunction(entry[attr]) ) {
|
||||||
|
$parentLi.data(attr + "Handler", entry[attr]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ( entry.disabled === true ) {
|
||||||
|
$parentLi.addClass("ui-state-disabled");
|
||||||
|
}
|
||||||
|
if ( entry.isHeader ) {
|
||||||
|
$parentLi.addClass("ui-widget-header");
|
||||||
|
}
|
||||||
|
if ( entry.addClass ) {
|
||||||
|
$parentLi.addClass(entry.addClass);
|
||||||
|
}
|
||||||
|
if ( $.isPlainObject(entry.data) ) {
|
||||||
|
$parentLi.data(entry.data);
|
||||||
|
}
|
||||||
|
if ( typeof entry.tooltip === "string" ) {
|
||||||
|
$parentLi.attr("title", entry.tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Convert a nested array of command objects into a <ul> structure. */
|
||||||
|
createMenuMarkup: function(options, $parentUl) {
|
||||||
|
var i, menu, $ul, $li;
|
||||||
|
if ( $parentUl == null ) {
|
||||||
|
$parentUl = $("<ul class='ui-helper-hidden' />").appendTo("body");
|
||||||
|
}
|
||||||
|
for (i = 0; i < options.length; i++) {
|
||||||
|
menu = options[i];
|
||||||
|
$li = $("<li/>").appendTo($parentUl);
|
||||||
|
|
||||||
|
$.moogle.contextmenu.createEntryMarkup(menu, $li);
|
||||||
|
|
||||||
|
if ( $.isArray(menu.children) ) {
|
||||||
|
$ul = $("<ul/>").appendTo($li);
|
||||||
|
$.moogle.contextmenu.createMenuMarkup(menu.children, $ul);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $parentUl;
|
||||||
|
},
|
||||||
|
/** Returns true if the menu item has child menu items */
|
||||||
|
isMenu: function(item) {
|
||||||
|
if ( isLTE110 ) {
|
||||||
|
return item.has(">a[aria-haspopup='true']").length > 0;
|
||||||
|
} else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
|
||||||
|
return item.is("[aria-haspopup='true']");
|
||||||
|
} else {
|
||||||
|
return item.has(">div[aria-haspopup='true']").length > 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Replace the title of elem', but retain icons andchild entries. */
|
||||||
|
replaceFirstTextNodeChild: function(elem, html) {
|
||||||
|
var $icons = elem.find(">span.ui-icon,>ul.ui-menu").detach();
|
||||||
|
|
||||||
|
elem
|
||||||
|
.empty()
|
||||||
|
.html(html)
|
||||||
|
.append($icons);
|
||||||
|
},
|
||||||
|
/** Updates the menu item's title */
|
||||||
|
updateTitle: function(item, title) {
|
||||||
|
if ( isLTE110 ) { // jQuery UI 1.10 and before used <a> tags
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild($("a", item), title);
|
||||||
|
} else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild(item, title);
|
||||||
|
} else { // jQuery UI 1.12+ introduced <div> tag wrappers
|
||||||
|
$.moogle.contextmenu.replaceFirstTextNodeChild($("div", item), title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
1
public/libraries/jquery.ui-contextmenu.min.js.map
Normal file
1
public/libraries/jquery.ui-contextmenu.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -4,11 +4,12 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const anonymization = require('../../services/anonymization');
|
const anonymization = require('../../services/anonymization');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/anonymize', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/anonymize', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await anonymization.anonymize();
|
await anonymization.anonymize();
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,9 +4,10 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const app_info = require('../../services/app_info');
|
const app_info = require('../../services/app_info');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkApiAuth, async (req, res, next) => {
|
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
res.send(app_info);
|
res.send(app_info);
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -7,8 +7,9 @@ const utils = require('../../services/utils');
|
|||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
const noteIdsToDelete = await sql.getFirstColumn("SELECT note_id FROM notes WHERE is_deleted = 1");
|
const noteIdsToDelete = await sql.getFirstColumn("SELECT note_id FROM notes WHERE is_deleted = 1");
|
||||||
const noteIdsSql = noteIdsToDelete
|
const noteIdsSql = noteIdsToDelete
|
||||||
@@ -34,14 +35,14 @@ router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, async (req, res, n
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/vacuum-database', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/vacuum-database', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.execute("VACUUM");
|
await sql.execute("VACUUM");
|
||||||
|
|
||||||
log.info("Database has been vacuumed.");
|
log.info("Database has been vacuumed.");
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,14 +4,15 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkApiAuth, async (req, res, next) => {
|
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await deleteOld();
|
await deleteOld();
|
||||||
|
|
||||||
const result = await sql.getAll("SELECT * FROM event_log ORDER BY date_added DESC");
|
const result = await sql.getAll("SELECT * FROM event_log ORDER BY date_added DESC");
|
||||||
|
|
||||||
res.send(result);
|
res.send(result);
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function deleteOld() {
|
async function deleteOld() {
|
||||||
const cutoffId = await sql.getFirstValue("SELECT id FROM event_log ORDER BY id DESC LIMIT 1000, 1");
|
const cutoffId = await sql.getFirstValue("SELECT id FROM event_log ORDER BY id DESC LIMIT 1000, 1");
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ const sql = require('../../services/sql');
|
|||||||
const data_dir = require('../../services/data_dir');
|
const data_dir = require('../../services/data_dir');
|
||||||
const html = require('html');
|
const html = require('html');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/:noteId/to/:directory', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ router.get('/:noteId/to/:directory', auth.checkApiAuth, async (req, res, next) =
|
|||||||
await exportNote(noteTreeId, completeExportDir);
|
await exportNote(noteTreeId, completeExportDir);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function exportNote(noteTreeId, dir) {
|
async function exportNote(noteTreeId, dir) {
|
||||||
const noteTree = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
const noteTree = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||||
|
|||||||
142
routes/api/image.js
Normal file
142
routes/api/image.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const sql = require('../../services/sql');
|
||||||
|
const auth = require('../../services/auth');
|
||||||
|
const utils = require('../../services/utils');
|
||||||
|
const sync_table = require('../../services/sync_table');
|
||||||
|
const multer = require('multer')();
|
||||||
|
const imagemin = require('imagemin');
|
||||||
|
const imageminMozJpeg = require('imagemin-mozjpeg');
|
||||||
|
const imageminPngQuant = require('imagemin-pngquant');
|
||||||
|
const imageminGifLossy = require('imagemin-giflossy');
|
||||||
|
const jimp = require('jimp');
|
||||||
|
const imageType = require('image-type');
|
||||||
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
|
router.get('/:imageId/:filename', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
|
||||||
|
const image = await sql.getFirst("SELECT * FROM images WHERE image_id = ?", [req.params.imageId]);
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
return res.status(404).send({});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set('Content-Type', 'image/' + image.format);
|
||||||
|
|
||||||
|
res.send(image.data);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
|
||||||
|
const sourceId = req.headers.source_id;
|
||||||
|
const noteId = req.query.noteId;
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
|
const note = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
return res.status(404).send(`Note ${noteId} doesn't exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) {
|
||||||
|
return res.status(400).send("Unknown image type: " + file.mimetype);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = utils.nowDate();
|
||||||
|
|
||||||
|
const resizedImage = await resize(file.buffer);
|
||||||
|
const optimizedImage = await optimize(resizedImage);
|
||||||
|
|
||||||
|
const imageFormat = imageType(optimizedImage);
|
||||||
|
|
||||||
|
const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, "");
|
||||||
|
const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext);
|
||||||
|
|
||||||
|
const imageId = utils.newImageId();
|
||||||
|
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sql.insert("images", {
|
||||||
|
image_id: imageId,
|
||||||
|
format: imageFormat.ext,
|
||||||
|
name: fileName,
|
||||||
|
checksum: utils.hash(optimizedImage),
|
||||||
|
data: optimizedImage,
|
||||||
|
is_deleted: 0,
|
||||||
|
date_modified: now,
|
||||||
|
date_created: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addImageSync(imageId, sourceId);
|
||||||
|
|
||||||
|
const noteImageId = utils.newNoteImageId();
|
||||||
|
|
||||||
|
await sql.insert("notes_image", {
|
||||||
|
note_image_id: noteImageId,
|
||||||
|
note_id: noteId,
|
||||||
|
image_id: imageId,
|
||||||
|
is_deleted: 0,
|
||||||
|
date_modified: now,
|
||||||
|
date_created: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(noteImageId, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
uploaded: true,
|
||||||
|
url: `/api/images/${imageId}/${fileName}`
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
const MAX_SIZE = 1000;
|
||||||
|
const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs
|
||||||
|
|
||||||
|
async function resize(buffer) {
|
||||||
|
const image = await jimp.read(buffer);
|
||||||
|
|
||||||
|
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) {
|
||||||
|
image.resize(MAX_SIZE, jimp.AUTO);
|
||||||
|
}
|
||||||
|
else if (image.bitmap.height > MAX_SIZE) {
|
||||||
|
image.resize(jimp.AUTO, MAX_SIZE);
|
||||||
|
}
|
||||||
|
else if (buffer.byteLength <= MAX_BYTE_SIZE) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do resizing with max quality which will be trimmed during optimization step next
|
||||||
|
image.quality(100);
|
||||||
|
|
||||||
|
// when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
|
||||||
|
image.background(0xFFFFFFFF);
|
||||||
|
|
||||||
|
// getBuffer doesn't support promises so this workaround
|
||||||
|
return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function optimize(buffer) {
|
||||||
|
return await imagemin.buffer(buffer, {
|
||||||
|
plugins: [
|
||||||
|
imageminMozJpeg({
|
||||||
|
quality: 50
|
||||||
|
}),
|
||||||
|
imageminPngQuant({
|
||||||
|
quality: "0-70"
|
||||||
|
}),
|
||||||
|
imageminGifLossy({
|
||||||
|
lossy: 80,
|
||||||
|
optimize: '3' // needs to be string
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -8,8 +8,9 @@ const data_dir = require('../../services/data_dir');
|
|||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, async (req, res, n
|
|||||||
await sql.doInTransaction(async () => await importNotes(dir, parentNoteId));
|
await sql.doInTransaction(async () => await importNotes(dir, parentNoteId));
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function importNotes(dir, parentNoteId) {
|
async function importNotes(dir, parentNoteId) {
|
||||||
const parent = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [parentNoteId]);
|
const parent = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [parentNoteId]);
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ const auth = require('../../services/auth');
|
|||||||
const password_encryption = require('../../services/password_encryption');
|
const password_encryption = require('../../services/password_encryption');
|
||||||
const protected_session = require('../../services/protected_session');
|
const protected_session = require('../../services/protected_session');
|
||||||
const app_info = require('../../services/app_info');
|
const app_info = require('../../services/app_info');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/sync', async (req, res, next) => {
|
router.post('/sync', wrap(async (req, res, next) => {
|
||||||
const timestampStr = req.body.timestamp;
|
const timestampStr = req.body.timestamp;
|
||||||
|
|
||||||
const timestamp = utils.parseDate(timestampStr);
|
const timestamp = utils.parseDate(timestampStr);
|
||||||
@@ -44,10 +45,10 @@ router.post('/sync', async (req, res, next) => {
|
|||||||
res.send({
|
res.send({
|
||||||
sourceId: source_id.getCurrentSourceId()
|
sourceId: source_id.getCurrentSourceId()
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
||||||
router.post('/protected', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/protected', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const password = req.body.password;
|
const password = req.body.password;
|
||||||
|
|
||||||
if (!await password_encryption.verifyPassword(password)) {
|
if (!await password_encryption.verifyPassword(password)) {
|
||||||
@@ -67,6 +68,6 @@ router.post('/protected', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
success: true,
|
success: true,
|
||||||
protectedSessionId: protectedSessionId
|
protectedSessionId: protectedSessionId
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -6,20 +6,21 @@ const auth = require('../../services/auth');
|
|||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
const migration = require('../../services/migration');
|
const migration = require('../../services/migration');
|
||||||
const app_info = require('../../services/app_info');
|
const app_info = require('../../services/app_info');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkApiAuthForMigrationPage, async (req, res, next) => {
|
router.get('', auth.checkApiAuthForMigrationPage, wrap(async (req, res, next) => {
|
||||||
res.send({
|
res.send({
|
||||||
db_version: parseInt(await options.getOption('db_version')),
|
db_version: parseInt(await options.getOption('db_version')),
|
||||||
app_db_version: app_info.db_version
|
app_db_version: app_info.db_version
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('', auth.checkApiAuthForMigrationPage, async (req, res, next) => {
|
router.post('', auth.checkApiAuthForMigrationPage, wrap(async (req, res, next) => {
|
||||||
const migrations = await migration.migrate();
|
const migrations = await migration.migrate();
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
migrations: migrations
|
migrations: migrations
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -7,8 +7,9 @@ const auth = require('../../services/auth');
|
|||||||
const data_encryption = require('../../services/data_encryption');
|
const data_encryption = require('../../services/data_encryption');
|
||||||
const protected_session = require('../../services/protected_session');
|
const protected_session = require('../../services/protected_session');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const history = await sql.getAll("SELECT * FROM notes_history WHERE note_id = ? order by date_modified_to desc", [noteId]);
|
const history = await sql.getAll("SELECT * FROM notes_history WHERE note_id = ? order by date_modified_to desc", [noteId]);
|
||||||
|
|
||||||
@@ -22,9 +23,9 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.send(history);
|
res.send(history);
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('', auth.checkApiAuth, async (req, res, next) => {
|
router.put('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
@@ -34,6 +35,6 @@ router.put('', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send();
|
res.send();
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -8,8 +8,9 @@ const notes = require('../../services/notes');
|
|||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
const protected_session = require('../../services/protected_session');
|
const protected_session = require('../../services/protected_session');
|
||||||
const data_encryption = require('../../services/data_encryption');
|
const data_encryption = require('../../services/data_encryption');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
const detail = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
const detail = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
||||||
@@ -30,9 +31,9 @@ router.get('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
res.send({
|
res.send({
|
||||||
detail: detail
|
detail: detail
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/:parentNoteId/children', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/:parentNoteId/children', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
const note = req.body;
|
const note = req.body;
|
||||||
@@ -43,9 +44,9 @@ router.post('/:parentNoteId/children', auth.checkApiAuth, async (req, res, next)
|
|||||||
'note_id': noteId,
|
'note_id': noteId,
|
||||||
'note_tree_id': noteTreeId
|
'note_tree_id': noteTreeId
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const note = req.body;
|
const note = req.body;
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
@@ -54,17 +55,17 @@ router.put('/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
await notes.updateNote(noteId, note, dataKey, sourceId);
|
await notes.updateNote(noteId, note, dataKey, sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.delete('/:noteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.delete('/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
await notes.deleteNote(req.params.noteTreeId, req.headers.source_id);
|
await notes.deleteNote(req.params.noteTreeId, req.headers.source_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const search = '%' + req.query.search + '%';
|
const search = '%' + req.query.search + '%';
|
||||||
|
|
||||||
const result = await sql.getAll("SELECT note_id FROM notes WHERE note_title LIKE ? OR note_text LIKE ?", [search, search]);
|
const result = await sql.getAll("SELECT note_id FROM notes WHERE note_title LIKE ? OR note_text LIKE ?", [search, search]);
|
||||||
@@ -76,6 +77,6 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.send(noteIdList);
|
res.send(noteIdList);
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -3,31 +3,25 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const utils = require('../../services/utils');
|
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const utils = require('../../services/utils');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
/**
|
||||||
|
* Code in this file deals with moving and cloning note tree rows. Relationship between note and parent note is unique
|
||||||
|
* for not deleted note trees. There may be multiple deleted note-parent note relationships.
|
||||||
|
*/
|
||||||
|
|
||||||
|
router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(parentNoteId, noteToMove.note_id);
|
if (!await validateParentChild(res, parentNoteId, noteToMove.note_id, noteTreeId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await checkTreeCycle(parentNoteId, noteToMove.note_id)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Moving note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
||||||
@@ -43,33 +37,20 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req,
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
const beforeNoteTreeId = req.params.beforeNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]);
|
const beforeNote = await getNoteTree(beforeNoteTreeId);
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(beforeNote.parent_note_id, noteToMove.note_id);
|
if (!await validateParentChild(res, beforeNote.parent_note_id, noteToMove.note_id, noteTreeId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await checkTreeCycle(beforeNote.parent_note_id, noteToMove.note_id)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Moving note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (beforeNote) {
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||||
@@ -87,37 +68,20 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
}
|
}));
|
||||||
else {
|
|
||||||
res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
const noteToMove = await getNoteTree(noteTreeId);
|
||||||
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
const afterNote = await getNoteTree(afterNoteTreeId);
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(afterNote.parent_note_id, noteToMove.note_id);
|
if (!await validateParentChild(res, afterNote.parent_note_id, noteToMove.note_id, noteTreeId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted && existing.note_tree_id !== noteTreeId) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await checkTreeCycle(afterNote.parent_note_id, noteToMove.note_id)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Moving note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (afterNote) {
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
// we don't change date_modified so other changes are prioritized in case of conflict
|
// we don't change date_modified so other changes are prioritized in case of conflict
|
||||||
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
// also we would have to sync all those modified note trees otherwise hash checks would fail
|
||||||
@@ -133,32 +97,16 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
}
|
}));
|
||||||
else {
|
|
||||||
res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const parentNoteId = req.params.parentNoteId;
|
||||||
const childNoteId = req.params.childNoteId;
|
const childNoteId = req.params.childNoteId;
|
||||||
const prefix = req.body.prefix;
|
const prefix = req.body.prefix;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
if (!await validateParentChild(res, parentNoteId, childNoteId)) {
|
||||||
|
return;
|
||||||
if (existing && !existing.is_deleted) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await checkTreeCycle(parentNoteId, childNoteId)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Cloning note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
||||||
@@ -184,33 +132,17 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const afterNoteTreeId = req.params.afterNoteTreeId;
|
const afterNoteTreeId = req.params.afterNoteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
const afterNote = await getNoteTree(afterNoteTreeId);
|
||||||
|
|
||||||
if (!afterNote) {
|
if (!await validateParentChild(res, afterNote.parent_note_id, noteId)) {
|
||||||
return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const existing = await getExistingNoteTree(afterNote.parent_note_id, noteId);
|
|
||||||
|
|
||||||
if (existing && !existing.is_deleted) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'This note already exists in target parent note.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await checkTreeCycle(afterNote.parent_note_id, noteId)) {
|
|
||||||
return res.send({
|
|
||||||
success: false,
|
|
||||||
message: 'Cloning note here would create cycle.'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
@@ -237,20 +169,48 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
|
async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
|
||||||
subTreeNoteIds.push(parentNoteId);
|
subTreeNoteIds.push(parentNoteId);
|
||||||
|
|
||||||
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ?", [parentNoteId]);
|
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [parentNoteId]);
|
||||||
|
|
||||||
for (const childNoteId of children) {
|
for (const childNoteId of children) {
|
||||||
await loadSubTreeNoteIds(childNoteId, subTreeNoteIds);
|
await loadSubTreeNoteIds(childNoteId, subTreeNoteIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getNoteTree(noteTreeId) {
|
||||||
|
return sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) {
|
||||||
|
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
||||||
|
|
||||||
|
if (existing && (noteTreeId === null || existing.note_tree_id !== noteTreeId)) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'This note already exists in the target.'
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await checkTreeCycle(parentNoteId, childNoteId)) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
message: 'Moving note here would create cycle.'
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function getExistingNoteTree(parentNoteId, childNoteId) {
|
async function getExistingNoteTree(parentNoteId, childNoteId) {
|
||||||
return await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ?', [childNoteId, parentNoteId]);
|
return await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ? AND is_deleted = 0', [childNoteId, parentNoteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,7 +233,7 @@ async function checkTreeCycle(parentNoteId, childNoteId) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]);
|
const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ? AND is_deleted = 0", [parentNoteId]);
|
||||||
|
|
||||||
for (const pid of parentNoteIds) {
|
for (const pid of parentNoteIds) {
|
||||||
if (!await checkTreeCycleInner(pid)) {
|
if (!await checkTreeCycleInner(pid)) {
|
||||||
@@ -287,7 +247,7 @@ async function checkTreeCycle(parentNoteId, childNoteId) {
|
|||||||
return await checkTreeCycleInner(parentNoteId);
|
return await checkTreeCycleInner(parentNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const expanded = req.params.expanded;
|
const expanded = req.params.expanded;
|
||||||
|
|
||||||
@@ -298,6 +258,6 @@ router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -5,11 +5,12 @@ const router = express.Router();
|
|||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const changePassword = require('../../services/change_password');
|
const changePassword = require('../../services/change_password');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/change', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/change', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const result = await changePassword.changePassword(req.body['current_password'], req.body['new_password'], req);
|
const result = await changePassword.changePassword(req.body['current_password'], req.body['new_password'], req);
|
||||||
|
|
||||||
res.send(result);
|
res.send(result);
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,8 +4,9 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const recentChanges = await sql.getAll(
|
const recentChanges = await sql.getAll(
|
||||||
`SELECT
|
`SELECT
|
||||||
notes.is_deleted AS current_is_deleted,
|
notes.is_deleted AS current_is_deleted,
|
||||||
@@ -19,6 +20,6 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
LIMIT 1000`);
|
LIMIT 1000`);
|
||||||
|
|
||||||
res.send(recentChanges);
|
res.send(recentChanges);
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -7,12 +7,13 @@ const auth = require('../../services/auth');
|
|||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkApiAuth, async (req, res, next) => {
|
router.get('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
res.send(await getRecentNotes());
|
res.send(await getRecentNotes());
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteTreeId/:notePath', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/:notePath', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const notePath = req.params.notePath;
|
const notePath = req.params.notePath;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
@@ -31,7 +32,7 @@ router.put('/:noteTreeId/:notePath', auth.checkApiAuth, async (req, res, next) =
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send(await getRecentNotes());
|
res.send(await getRecentNotes());
|
||||||
});
|
}));
|
||||||
|
|
||||||
async function getRecentNotes() {
|
async function getRecentNotes() {
|
||||||
return await sql.getAll(`
|
return await sql.getAll(`
|
||||||
|
|||||||
@@ -5,24 +5,25 @@ const router = express.Router();
|
|||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
// options allowed to be updated directly in settings dialog
|
// options allowed to be updated directly in settings dialog
|
||||||
const ALLOWED_OPTIONS = ['protected_session_timeout', 'history_snapshot_time_interval'];
|
const ALLOWED_OPTIONS = ['protected_session_timeout', 'history_snapshot_time_interval'];
|
||||||
|
|
||||||
router.get('/all', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/all', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const settings = await sql.getMap("SELECT opt_name, opt_value FROM options");
|
const settings = await sql.getMap("SELECT opt_name, opt_value FROM options");
|
||||||
|
|
||||||
res.send(settings);
|
res.send(settings);
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const settings = await sql.getMap("SELECT opt_name, opt_value FROM options WHERE opt_name IN ("
|
const settings = await sql.getMap("SELECT opt_name, opt_value FROM options WHERE opt_name IN ("
|
||||||
+ ALLOWED_OPTIONS.map(x => '?').join(",") + ")", ALLOWED_OPTIONS);
|
+ ALLOWED_OPTIONS.map(x => '?').join(",") + ")", ALLOWED_OPTIONS);
|
||||||
|
|
||||||
res.send(settings);
|
res.send(settings);
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const body = req.body;
|
const body = req.body;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
|
|
||||||
@@ -38,6 +39,6 @@ router.post('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
else {
|
else {
|
||||||
res.send("not allowed option to set");
|
res.send("not allowed option to set");
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -8,8 +8,9 @@ const sql = require('../../services/sql');
|
|||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
const my_scrypt = require('../../services/my_scrypt');
|
const my_scrypt = require('../../services/my_scrypt');
|
||||||
const password_encryption = require('../../services/password_encryption');
|
const password_encryption = require('../../services/password_encryption');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('', auth.checkAppNotInitialized, async (req, res, next) => {
|
router.post('', auth.checkAppNotInitialized, wrap(async (req, res, next) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
@@ -27,6 +28,6 @@ router.post('', auth.checkAppNotInitialized, async (req, res, next) => {
|
|||||||
sql.setDbReadyAsResolved();
|
sql.setDbReadyAsResolved();
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -4,8 +4,9 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('/execute', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/execute', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const query = req.body.query;
|
const query = req.body.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -20,6 +21,6 @@ router.post('/execute', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
error: e.message
|
error: e.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -10,19 +10,20 @@ const sql = require('../../services/sql');
|
|||||||
const options = require('../../services/options');
|
const options = require('../../services/options');
|
||||||
const content_hash = require('../../services/content_hash');
|
const content_hash = require('../../services/content_hash');
|
||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/check', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/check', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
res.send({
|
res.send({
|
||||||
'hashes': await content_hash.getHashes(),
|
'hashes': await content_hash.getHashes(),
|
||||||
'max_sync_id': await sql.getFirstValue('SELECT MAX(id) FROM sync')
|
'max_sync_id': await sql.getFirstValue('SELECT MAX(id) FROM sync')
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/now', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/now', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
res.send(await sync.sync());
|
res.send(await sync.sync());
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/fill-sync-rows', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/fill-sync-rows', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
await sync_table.fillAllSyncRows();
|
await sync_table.fillAllSyncRows();
|
||||||
});
|
});
|
||||||
@@ -30,9 +31,9 @@ router.post('/fill-sync-rows', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
log.info("Sync rows have been filled.");
|
log.info("Sync rows have been filled.");
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/force-full-sync', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/force-full-sync', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
await options.setOption('last_synced_pull', 0);
|
await options.setOption('last_synced_pull', 0);
|
||||||
await options.setOption('last_synced_push', 0);
|
await options.setOption('last_synced_push', 0);
|
||||||
@@ -44,9 +45,9 @@ router.post('/force-full-sync', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
sync.sync();
|
sync.sync();
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('/force-note-sync/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.post('/force-note-sync/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
@@ -68,35 +69,35 @@ router.post('/force-note-sync/:noteId', auth.checkApiAuth, async (req, res, next
|
|||||||
sync.sync();
|
sync.sync();
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/changed', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const lastSyncId = parseInt(req.query.lastSyncId);
|
const lastSyncId = parseInt(req.query.lastSyncId);
|
||||||
|
|
||||||
res.send(await sql.getAll("SELECT * FROM sync WHERE id > ?", [lastSyncId]));
|
res.send(await sql.getAll("SELECT * FROM sync WHERE id > ?", [lastSyncId]));
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/notes/:noteId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
entity: await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId])
|
entity: await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId])
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/notes_tree/:noteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/notes_tree/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
|
|
||||||
res.send(await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]));
|
res.send(await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]));
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/notes_history/:noteHistoryId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/notes_history/:noteHistoryId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteHistoryId = req.params.noteHistoryId;
|
const noteHistoryId = req.params.noteHistoryId;
|
||||||
|
|
||||||
res.send(await sql.getFirst("SELECT * FROM notes_history WHERE note_history_id = ?", [noteHistoryId]));
|
res.send(await sql.getFirst("SELECT * FROM notes_history WHERE note_history_id = ?", [noteHistoryId]));
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/options/:optName', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/options/:optName', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const optName = req.params.optName;
|
const optName = req.params.optName;
|
||||||
|
|
||||||
if (!options.SYNCED_OPTIONS.includes(optName)) {
|
if (!options.SYNCED_OPTIONS.includes(optName)) {
|
||||||
@@ -105,57 +106,86 @@ router.get('/options/:optName', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
else {
|
else {
|
||||||
res.send(await sql.getFirst("SELECT * FROM options WHERE opt_name = ?", [optName]));
|
res.send(await sql.getFirst("SELECT * FROM options WHERE opt_name = ?", [optName]));
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/notes_reordering/:noteTreeParentId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/notes_reordering/:noteTreeParentId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeParentId = req.params.noteTreeParentId;
|
const noteTreeParentId = req.params.noteTreeParentId;
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
parent_note_id: noteTreeParentId,
|
parent_note_id: noteTreeParentId,
|
||||||
ordering: await sql.getMap("SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ?", [noteTreeParentId])
|
ordering: await sql.getMap("SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [noteTreeParentId])
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/recent_notes/:noteTreeId', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/recent_notes/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
|
|
||||||
res.send(await sql.getFirst("SELECT * FROM recent_notes WHERE note_tree_id = ?", [noteTreeId]));
|
res.send(await sql.getFirst("SELECT * FROM recent_notes WHERE note_tree_id = ?", [noteTreeId]));
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/notes', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/images/:imageId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const imageId = req.params.imageId;
|
||||||
|
const entity = await sql.getFirst("SELECT * FROM images WHERE image_id = ?", [imageId]);
|
||||||
|
|
||||||
|
if (entity && entity.data !== null) {
|
||||||
|
entity.data = entity.data.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(entity);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/notes_image/:noteImageId', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
const noteImageId = req.params.noteImageId;
|
||||||
|
|
||||||
|
res.send(await sql.getFirst("SELECT * FROM notes_image WHERE note_image_id = ?", [noteImageId]));
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/notes_tree', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/notes_tree', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNoteTree(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNoteTree(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/notes_history', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/notes_history', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNoteHistory(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNoteHistory(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/notes_reordering', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/notes_reordering', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateNoteReordering(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateNoteReordering(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/options', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/options', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateOptions(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateOptions(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/recent_notes', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/recent_notes', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
await syncUpdate.updateRecentNotes(req.body.entity, req.body.sourceId);
|
await syncUpdate.updateRecentNotes(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
router.put('/images', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
await syncUpdate.updateImage(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/notes_image', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
|
await syncUpdate.updateNoteImage(req.body.entity, req.body.sourceId);
|
||||||
|
|
||||||
|
res.send({});
|
||||||
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -10,16 +10,23 @@ const protected_session = require('../../services/protected_session');
|
|||||||
const data_encryption = require('../../services/data_encryption');
|
const data_encryption = require('../../services/data_encryption');
|
||||||
const notes = require('../../services/notes');
|
const notes = require('../../services/notes');
|
||||||
const sync_table = require('../../services/sync_table');
|
const sync_table = require('../../services/sync_table');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const notes = await sql.getAll("SELECT "
|
const notes = await sql.getAll(`
|
||||||
+ "notes_tree.*, "
|
SELECT
|
||||||
+ "notes.note_title, "
|
notes_tree.*,
|
||||||
+ "notes.is_protected "
|
notes.note_title,
|
||||||
+ "FROM notes_tree "
|
notes.is_protected
|
||||||
+ "JOIN notes ON notes.note_id = notes_tree.note_id "
|
FROM
|
||||||
+ "WHERE notes.is_deleted = 0 AND notes_tree.is_deleted = 0 "
|
notes_tree
|
||||||
+ "ORDER BY note_position");
|
JOIN
|
||||||
|
notes ON notes.note_id = notes_tree.note_id
|
||||||
|
WHERE
|
||||||
|
notes.is_deleted = 0
|
||||||
|
AND notes_tree.is_deleted = 0
|
||||||
|
ORDER BY
|
||||||
|
note_position`);
|
||||||
|
|
||||||
const dataKey = protected_session.getDataKey(req);
|
const dataKey = protected_session.getDataKey(req);
|
||||||
|
|
||||||
@@ -33,9 +40,9 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
notes: notes,
|
notes: notes,
|
||||||
start_note_path: await options.getOption('start_note_path')
|
start_note_path: await options.getOption('start_note_path')
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteId/protect-sub-tree/:isProtected', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteId/protect-sub-tree/:isProtected', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
const isProtected = !!parseInt(req.params.isProtected);
|
const isProtected = !!parseInt(req.params.isProtected);
|
||||||
const dataKey = protected_session.getDataKey(req);
|
const dataKey = protected_session.getDataKey(req);
|
||||||
@@ -46,9 +53,9 @@ router.put('/:noteId/protect-sub-tree/:isProtected', auth.checkApiAuth, async (r
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.put('/:noteTreeId/set-prefix', auth.checkApiAuth, async (req, res, next) => {
|
router.put('/:noteTreeId/set-prefix', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||||
const noteTreeId = req.params.noteTreeId;
|
const noteTreeId = req.params.noteTreeId;
|
||||||
const sourceId = req.headers.source_id;
|
const sourceId = req.headers.source_id;
|
||||||
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
|
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
|
||||||
@@ -60,6 +67,6 @@ router.put('/:noteTreeId/set-prefix', auth.checkApiAuth, async (req, res, next)
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.send({});
|
res.send({});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ const router = express.Router();
|
|||||||
const auth = require('../services/auth');
|
const auth = require('../services/auth');
|
||||||
const source_id = require('../services/source_id');
|
const source_id = require('../services/source_id');
|
||||||
const sql = require('../services/sql');
|
const sql = require('../services/sql');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkAuth, async (req, res, next) => {
|
router.get('', auth.checkAuth, wrap(async (req, res, next) => {
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
sourceId: await source_id.generateSourceId(),
|
sourceId: await source_id.generateSourceId(),
|
||||||
maxSyncIdAtLoad: await sql.getFirstValue("SELECT MAX(id) FROM sync")
|
maxSyncIdAtLoad: await sql.getFirstValue("SELECT MAX(id) FROM sync")
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ const router = express.Router();
|
|||||||
const utils = require('../services/utils');
|
const utils = require('../services/utils');
|
||||||
const options = require('../services/options');
|
const options = require('../services/options');
|
||||||
const my_scrypt = require('../services/my_scrypt');
|
const my_scrypt = require('../services/my_scrypt');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', (req, res, next) => {
|
router.get('', wrap(async (req, res, next) => {
|
||||||
res.render('login', { 'failedAuth': false });
|
res.render('login', { 'failedAuth': false });
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.post('', async (req, res, next) => {
|
router.post('', wrap(async (req, res, next) => {
|
||||||
const userName = await options.getOption('username');
|
const userName = await options.getOption('username');
|
||||||
|
|
||||||
const guessedPassword = req.body.password;
|
const guessedPassword = req.body.password;
|
||||||
@@ -32,7 +33,7 @@ router.post('', async (req, res, next) => {
|
|||||||
else {
|
else {
|
||||||
res.render('login', {'failedAuth': true});
|
res.render('login', {'failedAuth': true});
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
|
||||||
async function verifyPassword(guessed_password) {
|
async function verifyPassword(guessed_password) {
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.post('', async (req, res, next) => {
|
router.post('', wrap(async (req, res, next) => {
|
||||||
req.session.regenerate(() => {
|
req.session.regenerate(() => {
|
||||||
req.session.loggedIn = false;
|
req.session.loggedIn = false;
|
||||||
|
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const auth = require('../services/auth');
|
const auth = require('../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkAuthForMigrationPage, (req, res, next) => {
|
router.get('', auth.checkAuthForMigrationPage, wrap(async (req, res, next) => {
|
||||||
res.render('migration', {});
|
res.render('migration', {});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const setupApiRoute = require('./api/setup');
|
|||||||
const sqlRoute = require('./api/sql');
|
const sqlRoute = require('./api/sql');
|
||||||
const anonymizationRoute = require('./api/anonymization');
|
const anonymizationRoute = require('./api/anonymization');
|
||||||
const cleanupRoute = require('./api/cleanup');
|
const cleanupRoute = require('./api/cleanup');
|
||||||
|
const imageRoute = require('./api/image');
|
||||||
|
|
||||||
function register(app) {
|
function register(app) {
|
||||||
app.use('/', indexRoute);
|
app.use('/', indexRoute);
|
||||||
@@ -51,6 +52,7 @@ function register(app) {
|
|||||||
app.use('/api/sql', sqlRoute);
|
app.use('/api/sql', sqlRoute);
|
||||||
app.use('/api/anonymization', anonymizationRoute);
|
app.use('/api/anonymization', anonymizationRoute);
|
||||||
app.use('/api/cleanup', cleanupRoute);
|
app.use('/api/cleanup', cleanupRoute);
|
||||||
|
app.use('/api/images', imageRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const auth = require('../services/auth');
|
const auth = require('../services/auth');
|
||||||
|
const wrap = require('express-promise-wrap').wrap;
|
||||||
|
|
||||||
router.get('', auth.checkAppNotInitialized, (req, res, next) => {
|
router.get('', auth.checkAppNotInitialized, wrap(async (req, res, next) => {
|
||||||
res.render('setup', {});
|
res.render('setup', {});
|
||||||
});
|
}));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
const build = require('./build');
|
const build = require('./build');
|
||||||
const packageJson = require('../package');
|
const packageJson = require('../package');
|
||||||
|
|
||||||
const APP_DB_VERSION = 61;
|
const APP_DB_VERSION = 65;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
app_version: packageJson.version,
|
app_version: packageJson.version,
|
||||||
|
|||||||
@@ -28,6 +28,20 @@ async function checkAuthForMigrationPage(req, res, next) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for electron things which need network stuff
|
||||||
|
// currently we're doing that for file upload because handling form data seems to be difficult
|
||||||
|
async function checkApiAuthOrElectron(req, res, next) {
|
||||||
|
if (!req.session.loggedIn && !utils.isElectron()) {
|
||||||
|
res.status(401).send("Not authorized");
|
||||||
|
}
|
||||||
|
else if (await sql.isDbUpToDate()) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.status(409).send("Mismatched app versions"); // need better response than that
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkApiAuth(req, res, next) {
|
async function checkApiAuth(req, res, next) {
|
||||||
if (!req.session.loggedIn) {
|
if (!req.session.loggedIn) {
|
||||||
res.status(401).send("Not authorized");
|
res.status(401).send("Not authorized");
|
||||||
@@ -63,5 +77,6 @@ module.exports = {
|
|||||||
checkAuthForMigrationPage,
|
checkAuthForMigrationPage,
|
||||||
checkApiAuth,
|
checkApiAuth,
|
||||||
checkApiAuthForMigrationPage,
|
checkApiAuthForMigrationPage,
|
||||||
checkAppNotInitialized
|
checkAppNotInitialized,
|
||||||
|
checkApiAuthOrElectron
|
||||||
};
|
};
|
||||||
@@ -6,6 +6,7 @@ const fs = require('fs-extra');
|
|||||||
const dataDir = require('./data_dir');
|
const dataDir = require('./data_dir');
|
||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
|
const sync_mutex = require('./sync_mutex');
|
||||||
|
|
||||||
async function regularBackup() {
|
async function regularBackup() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -21,6 +22,10 @@ async function regularBackup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function backupNow() {
|
async function backupNow() {
|
||||||
|
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
|
||||||
|
const releaseMutex = await sync_mutex.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
const now = utils.nowDate();
|
const now = utils.nowDate();
|
||||||
|
|
||||||
const backupFile = dataDir.BACKUP_DIR + "/" + "backup-" + utils.getDateTimeForFile() + ".db";
|
const backupFile = dataDir.BACKUP_DIR + "/" + "backup-" + utils.getDateTimeForFile() + ".db";
|
||||||
@@ -32,6 +37,10 @@ async function backupNow() {
|
|||||||
await sql.doInTransaction(async () => {
|
await sql.doInTransaction(async () => {
|
||||||
await options.setOption('last_backup_date', now);
|
await options.setOption('last_backup_date', now);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
releaseMutex();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupOldBackups() {
|
async function cleanupOldBackups() {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = { build_date:"2018-01-02T22:46:50-05:00", build_revision: "96a44a9a0c1b90ba7b58ef37a52cadbaffdf918d" };
|
module.exports = { build_date:"2018-01-07T10:04:46-05:00", build_revision: "488e657cc43fab879662a6da23b5a69dd7591b06" };
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const messaging = require('./messaging');
|
const messaging = require('./messaging');
|
||||||
|
const sync_mutex = require('./sync_mutex');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
async function runCheck(query, errorText, errorList) {
|
async function runCheck(query, errorText, errorList) {
|
||||||
|
utils.assertArguments(query, errorText, errorList);
|
||||||
|
|
||||||
const result = await sql.getFirstColumn(query);
|
const result = await sql.getFirstColumn(query);
|
||||||
|
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
@@ -19,7 +23,7 @@ async function runCheck(query, errorText, errorList) {
|
|||||||
|
|
||||||
async function checkTreeCycles(errorList) {
|
async function checkTreeCycles(errorList) {
|
||||||
const childToParents = {};
|
const childToParents = {};
|
||||||
const rows = await sql.getAll("SELECT note_id, parent_note_id FROM notes_tree");
|
const rows = await sql.getAll("SELECT note_id, parent_note_id FROM notes_tree WHERE is_deleted = 0");
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const childNoteId = row.note_id;
|
const childNoteId = row.note_id;
|
||||||
@@ -80,11 +84,9 @@ async function runSyncRowChecks(table, key, errorList) {
|
|||||||
`Missing ${table} records for existing sync rows`, errorList);
|
`Missing ${table} records for existing sync rows`, errorList);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runChecks() {
|
async function runAllChecks() {
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
|
|
||||||
const startTime = new Date();
|
|
||||||
|
|
||||||
await runCheck(`
|
await runCheck(`
|
||||||
SELECT
|
SELECT
|
||||||
note_id
|
note_id
|
||||||
@@ -139,7 +141,7 @@ async function runChecks() {
|
|||||||
WHERE
|
WHERE
|
||||||
(SELECT COUNT(*) FROM notes_tree WHERE notes.note_id = notes_tree.note_id AND notes_tree.is_deleted = 0) = 0
|
(SELECT COUNT(*) FROM notes_tree WHERE notes.note_id = notes_tree.note_id AND notes_tree.is_deleted = 0) = 0
|
||||||
AND notes.is_deleted = 0
|
AND notes.is_deleted = 0
|
||||||
`, );
|
`, 'No undeleted note trees for note IDs', errorList);
|
||||||
|
|
||||||
await runCheck(`
|
await runCheck(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -161,10 +163,36 @@ async function runChecks() {
|
|||||||
notes.note_id IS NULL`,
|
notes.note_id IS NULL`,
|
||||||
"Missing notes records for following note history ID > note ID", errorList);
|
"Missing notes records for following note history ID > note ID", errorList);
|
||||||
|
|
||||||
|
await runCheck(`
|
||||||
|
SELECT
|
||||||
|
notes_tree.parent_note_id || ' > ' || notes_tree.note_id
|
||||||
|
FROM
|
||||||
|
notes_tree
|
||||||
|
WHERE
|
||||||
|
notes_tree.is_deleted = 0
|
||||||
|
GROUP BY
|
||||||
|
notes_tree.parent_note_id,
|
||||||
|
notes_tree.note_id
|
||||||
|
HAVING
|
||||||
|
COUNT(*) > 1`,
|
||||||
|
"Duplicate undeleted parent note <-> note relationship - parent note ID > note ID", errorList);
|
||||||
|
|
||||||
|
await runCheck(`
|
||||||
|
SELECT
|
||||||
|
images.image_id
|
||||||
|
FROM
|
||||||
|
images
|
||||||
|
LEFT JOIN notes_image ON notes_image.image_id = images.image_id
|
||||||
|
WHERE
|
||||||
|
notes_image.note_image_id IS NULL`,
|
||||||
|
"Image with no note relation", errorList);
|
||||||
|
|
||||||
await runSyncRowChecks("notes", "note_id", errorList);
|
await runSyncRowChecks("notes", "note_id", errorList);
|
||||||
await runSyncRowChecks("notes_history", "note_history_id", errorList);
|
await runSyncRowChecks("notes_history", "note_history_id", errorList);
|
||||||
await runSyncRowChecks("notes_tree", "note_tree_id", errorList);
|
await runSyncRowChecks("notes_tree", "note_tree_id", errorList);
|
||||||
await runSyncRowChecks("recent_notes", "note_tree_id", errorList);
|
await runSyncRowChecks("recent_notes", "note_tree_id", errorList);
|
||||||
|
await runSyncRowChecks("images", "image_id", errorList);
|
||||||
|
await runSyncRowChecks("notes_image", "note_image_id", errorList);
|
||||||
|
|
||||||
if (errorList.length === 0) {
|
if (errorList.length === 0) {
|
||||||
// we run this only if basic checks passed since this assumes basic data consistency
|
// we run this only if basic checks passed since this assumes basic data consistency
|
||||||
@@ -172,7 +200,24 @@ async function runChecks() {
|
|||||||
await checkTreeCycles(errorList);
|
await checkTreeCycles(errorList);
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsedTimeMs = new Date().getTime() - startTime.getTime();
|
return errorList;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChecks() {
|
||||||
|
let errorList;
|
||||||
|
let elapsedTimeMs;
|
||||||
|
const releaseMutex = await sync_mutex.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = new Date();
|
||||||
|
|
||||||
|
errorList = await runAllChecks();
|
||||||
|
|
||||||
|
elapsedTimeMs = new Date().getTime() - startTime.getTime();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
releaseMutex();
|
||||||
|
}
|
||||||
|
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList));
|
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList));
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ async function getHashes() {
|
|||||||
const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(',');
|
const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(',');
|
||||||
|
|
||||||
const hashes = {
|
const hashes = {
|
||||||
notes: getHash(await sql.getAll(`SELECT
|
notes: getHash(await sql.getAll(`
|
||||||
|
SELECT
|
||||||
note_id,
|
note_id,
|
||||||
note_title,
|
note_title,
|
||||||
note_text,
|
note_text,
|
||||||
@@ -29,7 +30,8 @@ async function getHashes() {
|
|||||||
FROM notes
|
FROM notes
|
||||||
ORDER BY note_id`)),
|
ORDER BY note_id`)),
|
||||||
|
|
||||||
notes_tree: getHash(await sql.getAll(`SELECT
|
notes_tree: getHash(await sql.getAll(`
|
||||||
|
SELECT
|
||||||
note_tree_id,
|
note_tree_id,
|
||||||
note_id,
|
note_id,
|
||||||
parent_note_id,
|
parent_note_id,
|
||||||
@@ -40,7 +42,8 @@ async function getHashes() {
|
|||||||
FROM notes_tree
|
FROM notes_tree
|
||||||
ORDER BY note_tree_id`)),
|
ORDER BY note_tree_id`)),
|
||||||
|
|
||||||
notes_history: getHash(await sql.getAll(`SELECT
|
notes_history: getHash(await sql.getAll(`
|
||||||
|
SELECT
|
||||||
note_history_id,
|
note_history_id,
|
||||||
note_id,
|
note_id,
|
||||||
note_title,
|
note_title,
|
||||||
@@ -50,7 +53,8 @@ async function getHashes() {
|
|||||||
FROM notes_history
|
FROM notes_history
|
||||||
ORDER BY note_history_id`)),
|
ORDER BY note_history_id`)),
|
||||||
|
|
||||||
recent_notes: getHash(await sql.getAll(`SELECT
|
recent_notes: getHash(await sql.getAll(`
|
||||||
|
SELECT
|
||||||
note_tree_id,
|
note_tree_id,
|
||||||
note_path,
|
note_path,
|
||||||
date_accessed,
|
date_accessed,
|
||||||
@@ -58,12 +62,27 @@ async function getHashes() {
|
|||||||
FROM recent_notes
|
FROM recent_notes
|
||||||
ORDER BY note_path`)),
|
ORDER BY note_path`)),
|
||||||
|
|
||||||
options: getHash(await sql.getAll(`SELECT
|
options: getHash(await sql.getAll(`
|
||||||
|
SELECT
|
||||||
opt_name,
|
opt_name,
|
||||||
opt_value
|
opt_value
|
||||||
FROM options
|
FROM options
|
||||||
WHERE opt_name IN (${optionsQuestionMarks})
|
WHERE opt_name IN (${optionsQuestionMarks})
|
||||||
ORDER BY opt_name`, options.SYNCED_OPTIONS))
|
ORDER BY opt_name`, options.SYNCED_OPTIONS)),
|
||||||
|
|
||||||
|
// we don't include image data on purpose because they are quite large, checksum is good enough
|
||||||
|
// to represent the data anyway
|
||||||
|
images: getHash(await sql.getAll(`
|
||||||
|
SELECT
|
||||||
|
image_id,
|
||||||
|
format,
|
||||||
|
checksum,
|
||||||
|
name,
|
||||||
|
is_deleted,
|
||||||
|
date_modified,
|
||||||
|
date_created
|
||||||
|
FROM images
|
||||||
|
ORDER BY image_id`))
|
||||||
};
|
};
|
||||||
|
|
||||||
const elapseTimeMs = new Date().getTime() - startTime.getTime();
|
const elapseTimeMs = new Date().getTime() - startTime.getTime();
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ async function protectNoteRecursively(noteId, dataKey, protect, sourceId) {
|
|||||||
|
|
||||||
await protectNote(note, dataKey, protect, sourceId);
|
await protectNote(note, dataKey, protect, sourceId);
|
||||||
|
|
||||||
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ?", [noteId]);
|
const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [noteId]);
|
||||||
|
|
||||||
for (const childNoteId of children) {
|
for (const childNoteId of children) {
|
||||||
await protectNoteRecursively(childNoteId, dataKey, protect, sourceId);
|
await protectNoteRecursively(childNoteId, dataKey, protect, sourceId);
|
||||||
@@ -135,25 +135,7 @@ async function protectNoteHistory(noteId, dataKey, protect, sourceId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNote(noteId, newNote, dataKey, sourceId) {
|
async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
|
||||||
if (newNote.detail.is_protected) {
|
|
||||||
await encryptNote(newNote, dataKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const nowStr = utils.nowDate();
|
|
||||||
|
|
||||||
const historySnapshotTimeInterval = parseInt(await options.getOption('history_snapshot_time_interval'));
|
|
||||||
|
|
||||||
const historyCutoff = utils.dateStr(new Date(now.getTime() - historySnapshotTimeInterval * 1000));
|
|
||||||
|
|
||||||
const existingNoteHistoryId = await sql.getFirstValue(
|
|
||||||
"SELECT note_history_id FROM notes_history WHERE note_id = ? AND date_modified_to >= ?", [noteId, historyCutoff]);
|
|
||||||
|
|
||||||
await sql.doInTransaction(async () => {
|
|
||||||
const msSinceDateCreated = now.getTime() - utils.parseDate(newNote.detail.date_created).getTime();
|
|
||||||
|
|
||||||
if (!existingNoteHistoryId && msSinceDateCreated >= historySnapshotTimeInterval * 1000) {
|
|
||||||
const oldNote = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
const oldNote = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
||||||
|
|
||||||
if (oldNote.is_protected) {
|
if (oldNote.is_protected) {
|
||||||
@@ -174,7 +156,78 @@ async function updateNote(noteId, newNote, dataKey, sourceId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await sync_table.addNoteHistorySync(newNoteHistoryId, sourceId);
|
await sync_table.addNoteHistorySync(newNoteHistoryId, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNoteImages(noteId, noteText, sourceId) {
|
||||||
|
const existingNoteImages = await sql.getAll("SELECT * FROM notes_image WHERE note_id = ?", [noteId]);
|
||||||
|
const foundImageIds = [];
|
||||||
|
const now = utils.nowDate();
|
||||||
|
const re = /src="\/api\/images\/([a-zA-Z0-9]+)\//g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while (match = re.exec(noteText)) {
|
||||||
|
const imageId = match[1];
|
||||||
|
const existingNoteImage = existingNoteImages.find(ni => ni.image_id === imageId);
|
||||||
|
|
||||||
|
if (!existingNoteImage) {
|
||||||
|
const noteImageId = utils.newNoteImageId();
|
||||||
|
|
||||||
|
await sql.insert("notes_image", {
|
||||||
|
note_image_id: noteImageId,
|
||||||
|
note_id: noteId,
|
||||||
|
image_id: imageId,
|
||||||
|
is_deleted: 0,
|
||||||
|
date_modified: now,
|
||||||
|
date_created: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(noteImageId, sourceId);
|
||||||
}
|
}
|
||||||
|
else if (existingNoteImage.is_deleted) {
|
||||||
|
await sql.execute("UPDATE notes_image SET is_deleted = 0, date_modified = ? WHERE note_image_id = ?",
|
||||||
|
[now, existingNoteImage.note_image_id]);
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(existingNoteImage.note_image_id, sourceId);
|
||||||
|
}
|
||||||
|
// else we don't need to do anything
|
||||||
|
|
||||||
|
foundImageIds.push(imageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// marking note images as deleted if they are not present on the page anymore
|
||||||
|
const unusedNoteImages = existingNoteImages.filter(ni => !foundImageIds.includes(ni.image_id));
|
||||||
|
|
||||||
|
for (const unusedNoteImage of unusedNoteImages) {
|
||||||
|
await sql.execute("UPDATE notes_image SET is_deleted = 1, date_modified = ? WHERE note_image_id = ?",
|
||||||
|
[now, unusedNoteImage.note_image_id]);
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(unusedNoteImage.note_image_id, sourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateNote(noteId, newNote, dataKey, sourceId) {
|
||||||
|
if (newNote.detail.is_protected) {
|
||||||
|
await encryptNote(newNote, dataKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const nowStr = utils.nowDate();
|
||||||
|
|
||||||
|
const historySnapshotTimeInterval = parseInt(await options.getOption('history_snapshot_time_interval'));
|
||||||
|
|
||||||
|
const historyCutoff = utils.dateStr(new Date(now.getTime() - historySnapshotTimeInterval * 1000));
|
||||||
|
|
||||||
|
const existingNoteHistoryId = await sql.getFirstValue(
|
||||||
|
"SELECT note_history_id FROM notes_history WHERE note_id = ? AND date_modified_to >= ?", [noteId, historyCutoff]);
|
||||||
|
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
const msSinceDateCreated = now.getTime() - utils.parseDate(newNote.detail.date_created).getTime();
|
||||||
|
|
||||||
|
if (!existingNoteHistoryId && msSinceDateCreated >= historySnapshotTimeInterval * 1000) {
|
||||||
|
await saveNoteHistory(noteId, dataKey, sourceId, nowStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveNoteImages(noteId, newNote.detail.note_text, sourceId);
|
||||||
|
|
||||||
await protectNoteHistory(noteId, dataKey, newNote.detail.is_protected);
|
await protectNoteHistory(noteId, dataKey, newNote.detail.is_protected);
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ const dbReady = new Promise((resolve, reject) => {
|
|||||||
await executeScript(notesSql);
|
await executeScript(notesSql);
|
||||||
await executeScript(notesTreeSql);
|
await executeScript(notesTreeSql);
|
||||||
|
|
||||||
const noteId = await getFirstValue("SELECT note_id FROM notes_tree WHERE parent_note_id = 'root' ORDER BY note_position");
|
const startNoteId = await getFirstValue("SELECT note_id FROM notes_tree WHERE parent_note_id = 'root' AND is_deleted = 0 ORDER BY note_position");
|
||||||
|
|
||||||
await require('./options').initOptions(noteId);
|
await require('./options').initOptions(startNoteId);
|
||||||
await require('./sync_table').fillAllSyncRows();
|
await require('./sync_table').fillAllSyncRows();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,22 +14,13 @@ const fs = require('fs');
|
|||||||
const app_info = require('./app_info');
|
const app_info = require('./app_info');
|
||||||
const messaging = require('./messaging');
|
const messaging = require('./messaging');
|
||||||
const sync_setup = require('./sync_setup');
|
const sync_setup = require('./sync_setup');
|
||||||
|
const sync_mutex = require('./sync_mutex');
|
||||||
|
|
||||||
let syncInProgress = false;
|
|
||||||
let proxyToggle = true;
|
let proxyToggle = true;
|
||||||
let syncServerCertificate = null;
|
let syncServerCertificate = null;
|
||||||
|
|
||||||
async function sync() {
|
async function sync() {
|
||||||
if (syncInProgress) {
|
const releaseMutex = await sync_mutex.acquire();
|
||||||
log.info("Sync already in progress");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Sync already in progress"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
syncInProgress = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!await sql.isDbUpToDate()) {
|
if (!await sql.isDbUpToDate()) {
|
||||||
@@ -74,7 +65,7 @@ async function sync() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
syncInProgress = false;
|
releaseMutex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +143,12 @@ async function pullSync(syncContext) {
|
|||||||
else if (sync.entity_name === 'recent_notes') {
|
else if (sync.entity_name === 'recent_notes') {
|
||||||
await syncUpdate.updateRecentNotes(resp, syncContext.sourceId);
|
await syncUpdate.updateRecentNotes(resp, syncContext.sourceId);
|
||||||
}
|
}
|
||||||
|
else if (sync.entity_name === 'images') {
|
||||||
|
await syncUpdate.updateImage(resp, syncContext.sourceId);
|
||||||
|
}
|
||||||
|
else if (sync.entity_name === 'notes_image') {
|
||||||
|
await syncUpdate.updateNoteImage(resp, syncContext.sourceId);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(`Unrecognized entity type ${sync.entity_name} in sync #${sync.id}`);
|
throw new Error(`Unrecognized entity type ${sync.entity_name} in sync #${sync.id}`);
|
||||||
}
|
}
|
||||||
@@ -214,7 +211,7 @@ async function pushEntity(sync, syncContext) {
|
|||||||
else if (sync.entity_name === 'notes_reordering') {
|
else if (sync.entity_name === 'notes_reordering') {
|
||||||
entity = {
|
entity = {
|
||||||
parent_note_id: sync.entity_id,
|
parent_note_id: sync.entity_id,
|
||||||
ordering: await sql.getMap('SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ?', [sync.entity_id])
|
ordering: await sql.getMap('SELECT note_tree_id, note_position FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [sync.entity_id])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (sync.entity_name === 'options') {
|
else if (sync.entity_name === 'options') {
|
||||||
@@ -223,6 +220,16 @@ async function pushEntity(sync, syncContext) {
|
|||||||
else if (sync.entity_name === 'recent_notes') {
|
else if (sync.entity_name === 'recent_notes') {
|
||||||
entity = await sql.getFirst('SELECT * FROM recent_notes WHERE note_tree_id = ?', [sync.entity_id]);
|
entity = await sql.getFirst('SELECT * FROM recent_notes WHERE note_tree_id = ?', [sync.entity_id]);
|
||||||
}
|
}
|
||||||
|
else if (sync.entity_name === 'images') {
|
||||||
|
entity = await sql.getFirst('SELECT * FROM images WHERE image_id = ?', [sync.entity_id]);
|
||||||
|
|
||||||
|
if (entity.data !== null) {
|
||||||
|
entity.data = entity.data.toString('base64');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sync.entity_name === 'notes_image') {
|
||||||
|
entity = await sql.getFirst('SELECT * FROM notes_image WHERE note_image_id = ?', [sync.entity_id]);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(`Unrecognized entity type ${sync.entity_name} in sync #${sync.id}`);
|
throw new Error(`Unrecognized entity type ${sync.entity_name} in sync #${sync.id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
8
services/sync_mutex.js
Normal file
8
services/sync_mutex.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Sync makes process can make data intermittently inconsistent. Processes which require strong data consistency
|
||||||
|
* (like consistency checks) can use this mutex to make sure sync isn't currently running.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
|
module.exports = new Mutex();
|
||||||
@@ -28,6 +28,14 @@ async function addRecentNoteSync(noteTreeId, sourceId) {
|
|||||||
await addEntitySync("recent_notes", noteTreeId, sourceId);
|
await addEntitySync("recent_notes", noteTreeId, sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addImageSync(imageId, sourceId) {
|
||||||
|
await addEntitySync("images", imageId, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addNoteImageSync(noteImageId, sourceId) {
|
||||||
|
await addEntitySync("notes_image", noteImageId, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
async function addEntitySync(entityName, entityId, sourceId) {
|
async function addEntitySync(entityName, entityId, sourceId) {
|
||||||
await sql.replace("sync", {
|
await sql.replace("sync", {
|
||||||
entity_name: entityName,
|
entity_name: entityName,
|
||||||
@@ -78,6 +86,8 @@ async function fillAllSyncRows() {
|
|||||||
await fillSyncRows("notes_tree", "note_tree_id");
|
await fillSyncRows("notes_tree", "note_tree_id");
|
||||||
await fillSyncRows("notes_history", "note_history_id");
|
await fillSyncRows("notes_history", "note_history_id");
|
||||||
await fillSyncRows("recent_notes", "note_tree_id");
|
await fillSyncRows("recent_notes", "note_tree_id");
|
||||||
|
await fillSyncRows("images", "image_id");
|
||||||
|
await fillSyncRows("notes_image", "note_image_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -87,6 +97,8 @@ module.exports = {
|
|||||||
addNoteHistorySync,
|
addNoteHistorySync,
|
||||||
addOptionsSync,
|
addOptionsSync,
|
||||||
addRecentNoteSync,
|
addRecentNoteSync,
|
||||||
|
addImageSync,
|
||||||
|
addNoteImageSync,
|
||||||
cleanupSyncRowsForMissingEntities,
|
cleanupSyncRowsForMissingEntities,
|
||||||
fillAllSyncRows
|
fillAllSyncRows
|
||||||
};
|
};
|
||||||
@@ -92,11 +92,45 @@ async function updateRecentNotes(entity, sourceId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateImage(entity, sourceId) {
|
||||||
|
if (entity.data !== null) {
|
||||||
|
entity.data = Buffer.from(entity.data, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
const origImage = await sql.getFirst("SELECT * FROM images WHERE image_id = ?", [entity.image_id]);
|
||||||
|
|
||||||
|
if (!origImage || origImage.date_modified <= entity.date_modified) {
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sql.replace("images", entity);
|
||||||
|
|
||||||
|
await sync_table.addImageSync(entity.image_id, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Update/sync image " + entity.image_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateNoteImage(entity, sourceId) {
|
||||||
|
const origNoteImage = await sql.getFirst("SELECT * FROM notes_image WHERE note_image_id = ?", [entity.note_image_id]);
|
||||||
|
|
||||||
|
if (!origNoteImage || origNoteImage.date_modified <= entity.date_modified) {
|
||||||
|
await sql.doInTransaction(async () => {
|
||||||
|
await sql.replace("notes_image", entity);
|
||||||
|
|
||||||
|
await sync_table.addNoteImageSync(entity.note_image_id, sourceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Update/sync note image " + entity.note_image_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
updateNote,
|
updateNote,
|
||||||
updateNoteTree,
|
updateNoteTree,
|
||||||
updateNoteHistory,
|
updateNoteHistory,
|
||||||
updateNoteReordering,
|
updateNoteReordering,
|
||||||
updateOptions,
|
updateOptions,
|
||||||
updateRecentNotes
|
updateRecentNotes,
|
||||||
|
updateImage,
|
||||||
|
updateNoteImage
|
||||||
};
|
};
|
||||||
@@ -15,6 +15,14 @@ function newNoteHistoryId() {
|
|||||||
return randomString(12);
|
return randomString(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newImageId() {
|
||||||
|
return randomString(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
function newNoteImageId() {
|
||||||
|
return randomString(12);
|
||||||
|
}
|
||||||
|
|
||||||
function randomString(length) {
|
function randomString(length) {
|
||||||
return randtoken.generate(length);
|
return randtoken.generate(length);
|
||||||
}
|
}
|
||||||
@@ -79,6 +87,14 @@ function sanitizeSql(str) {
|
|||||||
return str.replace(/'/g, "\\'");
|
return str.replace(/'/g, "\\'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertArguments() {
|
||||||
|
for (const i in arguments) {
|
||||||
|
if (!arguments[i]) {
|
||||||
|
throw new Error(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
randomSecureToken,
|
randomSecureToken,
|
||||||
randomString,
|
randomString,
|
||||||
@@ -88,6 +104,8 @@ module.exports = {
|
|||||||
newNoteId,
|
newNoteId,
|
||||||
newNoteTreeId,
|
newNoteTreeId,
|
||||||
newNoteHistoryId,
|
newNoteHistoryId,
|
||||||
|
newImageId,
|
||||||
|
newNoteImageId,
|
||||||
toBase64,
|
toBase64,
|
||||||
fromBase64,
|
fromBase64,
|
||||||
hmac,
|
hmac,
|
||||||
@@ -95,5 +113,6 @@ module.exports = {
|
|||||||
hash,
|
hash,
|
||||||
isEmptyOrWhitespace,
|
isEmptyOrWhitespace,
|
||||||
getDateTimeForFile,
|
getDateTimeForFile,
|
||||||
sanitizeSql
|
sanitizeSql,
|
||||||
|
assertArguments
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user