mirror of
https://github.com/zadam/trilium.git
synced 2025-10-30 01:36:24 +01:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86b1410952 | ||
|
|
29eb88bac3 | ||
|
|
31b4186e17 | ||
|
|
bde9e825c8 | ||
|
|
0e8285a7e4 | ||
|
|
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 |
10
bin/build.sh
10
bin/build.sh
@@ -4,9 +4,11 @@ echo "Deleting dist"
|
||||
|
||||
rm -r dist/*
|
||||
|
||||
cp -r ../trilium-node-binaries/sqlite/* node_modules/sqlite3/lib/binding/
|
||||
|
||||
cp -r ../trilium-node-binaries/scrypt/* node_modules/scrypt/bin/
|
||||
cp -r bin/deps/sqlite/* node_modules/sqlite3/lib/binding/
|
||||
cp -r bin/deps/scrypt/* node_modules/scrypt/bin/
|
||||
cp -r bin/deps/image/cjpeg.exe node_modules/mozjpeg/vendor/
|
||||
cp -r bin/deps/image/pngquant.exe node_modules/pngquant-bin/vendor/
|
||||
cp -r bin/deps/image/gifsicle.exe node_modules/giflossy/vendor/
|
||||
|
||||
./node_modules/.bin/electron-rebuild --arch=ia32
|
||||
|
||||
@@ -19,4 +21,4 @@ cp -r ../trilium-node-binaries/scrypt/* node_modules/scrypt/bin/
|
||||
./node_modules/.bin/electron-packager . --out=dist --platform=win32 --arch=x64 --overwrite
|
||||
|
||||
# can't copy this before the packaging because the same file name is used for both linux and windows build
|
||||
cp ../trilium-node-binaries/scrypt.node ./dist/trilium-win32-x64/resources/app/node_modules/scrypt/build/Release/
|
||||
cp bin/deps/scrypt/win32-x64-57/scrypt.node ./dist/trilium-win32-x64/resources/app/node_modules/scrypt/build/Release/
|
||||
|
||||
BIN
bin/deps/image/cjpeg.exe
Normal file
BIN
bin/deps/image/cjpeg.exe
Normal file
Binary file not shown.
BIN
bin/deps/image/gifsicle.exe
Normal file
BIN
bin/deps/image/gifsicle.exe
Normal file
Binary file not shown.
BIN
bin/deps/image/pngquant.exe
Normal file
BIN
bin/deps/image/pngquant.exe
Normal file
Binary file not shown.
BIN
bin/deps/scrypt/win32-x64-57/scrypt.node
Normal file
BIN
bin/deps/scrypt/win32-x64-57/scrypt.node
Normal file
Binary file not shown.
BIN
bin/deps/sqlite/electron-v1.8-win32-x64/node_sqlite3.node
Normal file
BIN
bin/deps/sqlite/electron-v1.8-win32-x64/node_sqlite3.node
Normal file
Binary file not shown.
BIN
bin/deps/sqlite/node-v57-win32-x64/node_sqlite3.node
Normal file
BIN
bin/deps/sqlite/node-v57-win32-x64/node_sqlite3.node
Normal file
Binary file not shown.
BIN
db/image-deleted.png
Normal file
BIN
db/image-deleted.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -67,10 +67,7 @@ CREATE INDEX `IDX_sync_sync_date` ON `sync` (
|
||||
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
|
||||
`is_deleted`
|
||||
);
|
||||
CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` (
|
||||
`note_tree_id`
|
||||
);
|
||||
CREATE UNIQUE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
|
||||
`note_id`,
|
||||
`parent_note_id`
|
||||
);
|
||||
@@ -86,3 +83,28 @@ CREATE INDEX `IDX_notes_history_note_date_modified_from` ON `notes_history` (
|
||||
CREATE INDEX `IDX_notes_history_note_date_modified_to` ON `notes_history` (
|
||||
`date_modified_to`
|
||||
);
|
||||
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);
|
||||
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",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zadam/trilium.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./bin/www",
|
||||
"test-electron": "xo",
|
||||
@@ -14,6 +19,7 @@
|
||||
"publish-forge": "electron-forge publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.1.3",
|
||||
"body-parser": "~1.18.2",
|
||||
"cookie-parser": "~1.4.3",
|
||||
"debug": "~3.1.0",
|
||||
@@ -23,15 +29,24 @@
|
||||
"electron-debug": "^1.0.0",
|
||||
"electron-in-page-search": "^1.2.4",
|
||||
"express": "~4.16.2",
|
||||
"express-promise-wrap": "^0.2.2",
|
||||
"express-session": "^1.15.6",
|
||||
"fs-extra": "^4.0.2",
|
||||
"helmet": "^3.9.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",
|
||||
"jimp": "^0.2.28",
|
||||
"multer": "^1.3.0",
|
||||
"rand-token": "^0.4.0",
|
||||
"request": "^2.83.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"sanitize-filename": "^1.6.1",
|
||||
"scrypt": "^6.0.3",
|
||||
"serve-favicon": "~2.4.5",
|
||||
"session-file-store": "^1.1.2",
|
||||
|
||||
@@ -157,6 +157,7 @@ settings.addModule((async function () {
|
||||
const fillSyncRowsButton = $("#fill-sync-rows-button");
|
||||
const anonymizeButton = $("#anonymize-button");
|
||||
const cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
|
||||
const cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
|
||||
const vacuumDatabaseButton = $("#vacuum-database-button");
|
||||
|
||||
forceFullSyncButton.click(async () => {
|
||||
@@ -186,6 +187,14 @@ settings.addModule((async function () {
|
||||
}
|
||||
});
|
||||
|
||||
cleanupUnusedImagesButton.click(async () => {
|
||||
if (confirm("Do you really want to clean up unused images?")) {
|
||||
await server.post('cleanup/cleanup-unused-images');
|
||||
|
||||
showMessage("Unused images have been cleaned up");
|
||||
}
|
||||
});
|
||||
|
||||
vacuumDatabaseButton.click(async () => {
|
||||
await server.post('cleanup/vacuum-database');
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ $(document).bind('keydown', "ctrl+shift+down", () => {
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#note-title").bind('keydown', 'return', () => $("#note-detail").focus());
|
||||
|
||||
$(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 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);
|
||||
|
||||
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");
|
||||
|
||||
noteTree.reload();
|
||||
@@ -47,6 +49,9 @@ const messaging = (function() {
|
||||
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);
|
||||
}
|
||||
else if (message.type === 'sync-hash-check-failed') {
|
||||
|
||||
@@ -490,12 +490,14 @@ const noteTree = (function() {
|
||||
|
||||
return false;
|
||||
},
|
||||
"ctrl+return": node => {
|
||||
noteDetailEl.focus();
|
||||
},
|
||||
"return": node => {
|
||||
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
|
||||
// after opening context menu, standard shortcuts don't work, but they are detected here
|
||||
// so we essentially takeover the standard handling with our implementation.
|
||||
@@ -532,13 +534,15 @@ const noteTree = (function() {
|
||||
const node = data.node;
|
||||
|
||||
if (targetType === 'title' || targetType === 'icon') {
|
||||
node.setActive();
|
||||
|
||||
if (!event.ctrlKey) {
|
||||
node.setActive();
|
||||
node.setSelected(true);
|
||||
|
||||
clearSelectedNodes();
|
||||
}
|
||||
|
||||
node.setSelected(true);
|
||||
else {
|
||||
node.setSelected(!node.isSelected());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ const server = (function() {
|
||||
get,
|
||||
post,
|
||||
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 anonymization = require('../../services/anonymization');
|
||||
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();
|
||||
|
||||
res.send({});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -4,9 +4,10 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const app_info = require('../../services/app_info');
|
||||
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);
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -7,8 +7,9 @@ const utils = require('../../services/utils');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const auth = require('../../services/auth');
|
||||
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 () => {
|
||||
const noteIdsToDelete = await sql.getFirstColumn("SELECT note_id FROM notes WHERE is_deleted = 1");
|
||||
const noteIdsSql = noteIdsToDelete
|
||||
@@ -21,6 +22,10 @@ router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, async (req, res, n
|
||||
|
||||
await sql.execute("DELETE FROM notes_tree WHERE is_deleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM notes_image WHERE is_deleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM images WHERE is_deleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM notes WHERE is_deleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM recent_notes");
|
||||
@@ -34,14 +39,41 @@ router.post('/cleanup-soft-deleted-items', auth.checkApiAuth, async (req, res, n
|
||||
});
|
||||
|
||||
res.send({});
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('/vacuum-database', auth.checkApiAuth, async (req, res, next) => {
|
||||
router.post('/cleanup-unused-images', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
const unusedImageIds = await sql.getFirstColumn(`
|
||||
SELECT images.image_id
|
||||
FROM images
|
||||
LEFT JOIN notes_image ON notes_image.image_id = images.image_id AND notes_image.is_deleted = 0
|
||||
WHERE
|
||||
images.is_deleted = 0
|
||||
AND notes_image.note_image_id IS NULL`);
|
||||
|
||||
const now = utils.nowDate();
|
||||
|
||||
for (const imageId of unusedImageIds) {
|
||||
log.info(`Deleting unused image: ${imageId}`);
|
||||
|
||||
await sql.execute("UPDATE images SET is_deleted = 1, data = null, date_modified = ? WHERE image_id = ?",
|
||||
[now, imageId]);
|
||||
|
||||
await sync_table.addImageSync(imageId, sourceId);
|
||||
}
|
||||
});
|
||||
|
||||
res.send({});
|
||||
}));
|
||||
|
||||
router.post('/vacuum-database', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
await sql.execute("VACUUM");
|
||||
|
||||
log.info("Database has been vacuumed.");
|
||||
|
||||
res.send({});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -4,14 +4,15 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
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();
|
||||
|
||||
const result = await sql.getAll("SELECT * FROM event_log ORDER BY date_added DESC");
|
||||
|
||||
res.send(result);
|
||||
});
|
||||
}));
|
||||
|
||||
async function deleteOld() {
|
||||
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 html = require('html');
|
||||
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 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);
|
||||
|
||||
res.send({});
|
||||
});
|
||||
}));
|
||||
|
||||
async function exportNote(noteTreeId, dir) {
|
||||
const noteTree = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||
|
||||
148
routes/api/image.js
Normal file
148
routes/api/image.js
Normal file
@@ -0,0 +1,148 @@
|
||||
"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;
|
||||
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
|
||||
const fs = require('fs');
|
||||
|
||||
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({});
|
||||
}
|
||||
else if (image.data === null) {
|
||||
res.set('Content-Type', 'image/png');
|
||||
return res.send(fs.readFileSync(RESOURCE_DIR + '/db/image-deleted.png'));
|
||||
}
|
||||
|
||||
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 sync_table = require('../../services/sync_table');
|
||||
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 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));
|
||||
|
||||
res.send({});
|
||||
});
|
||||
}));
|
||||
|
||||
async function importNotes(dir, 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 protected_session = require('../../services/protected_session');
|
||||
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 timestamp = utils.parseDate(timestampStr);
|
||||
@@ -44,10 +45,10 @@ router.post('/sync', async (req, res, next) => {
|
||||
res.send({
|
||||
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)
|
||||
router.post('/protected', auth.checkApiAuth, async (req, res, next) => {
|
||||
router.post('/protected', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const password = req.body.password;
|
||||
|
||||
if (!await password_encryption.verifyPassword(password)) {
|
||||
@@ -67,6 +68,6 @@ router.post('/protected', auth.checkApiAuth, async (req, res, next) => {
|
||||
success: true,
|
||||
protectedSessionId: protectedSessionId
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -6,20 +6,21 @@ const auth = require('../../services/auth');
|
||||
const options = require('../../services/options');
|
||||
const migration = require('../../services/migration');
|
||||
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({
|
||||
db_version: parseInt(await options.getOption('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();
|
||||
|
||||
res.send({
|
||||
migrations: migrations
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -7,8 +7,9 @@ const auth = require('../../services/auth');
|
||||
const data_encryption = require('../../services/data_encryption');
|
||||
const protected_session = require('../../services/protected_session');
|
||||
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 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);
|
||||
});
|
||||
}));
|
||||
|
||||
router.put('', auth.checkApiAuth, async (req, res, next) => {
|
||||
router.put('', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
@@ -34,6 +35,6 @@ router.put('', auth.checkApiAuth, async (req, res, next) => {
|
||||
});
|
||||
|
||||
res.send();
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -8,8 +8,9 @@ const notes = require('../../services/notes');
|
||||
const log = require('../../services/log');
|
||||
const protected_session = require('../../services/protected_session');
|
||||
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 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({
|
||||
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 parentNoteId = req.params.parentNoteId;
|
||||
const note = req.body;
|
||||
@@ -43,9 +44,9 @@ router.post('/:parentNoteId/children', auth.checkApiAuth, async (req, res, next)
|
||||
'note_id': noteId,
|
||||
'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 noteId = req.params.noteId;
|
||||
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);
|
||||
|
||||
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 notes.deleteNote(req.params.noteTreeId, req.headers.source_id);
|
||||
});
|
||||
|
||||
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 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);
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -3,31 +3,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const utils = require('../../services/utils');
|
||||
const auth = require('../../services/auth');
|
||||
const utils = require('../../services/utils');
|
||||
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 parentNoteId = req.params.parentNoteId;
|
||||
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 (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.'
|
||||
});
|
||||
if (!await validateParentChild(res, parentNoteId, noteToMove.note_id, noteTreeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]);
|
||||
@@ -43,122 +37,76 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req,
|
||||
});
|
||||
|
||||
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 beforeNoteTreeId = req.params.beforeNoteTreeId;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||
const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]);
|
||||
const noteToMove = await getNoteTree(noteTreeId);
|
||||
const beforeNote = await getNoteTree(beforeNoteTreeId);
|
||||
|
||||
const existing = await getExistingNoteTree(beforeNote.parent_note_id, noteToMove.note_id);
|
||||
|
||||
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 validateParentChild(res, beforeNote.parent_note_id, noteToMove.note_id, noteTreeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await checkTreeCycle(beforeNote.parent_note_id, noteToMove.note_id)) {
|
||||
return res.send({
|
||||
success: false,
|
||||
message: 'Moving note here would create cycle.'
|
||||
});
|
||||
}
|
||||
await sql.doInTransaction(async () => {
|
||||
// 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
|
||||
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0",
|
||||
[beforeNote.parent_note_id, beforeNote.note_position]);
|
||||
|
||||
if (beforeNote) {
|
||||
await sql.doInTransaction(async () => {
|
||||
// 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
|
||||
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0",
|
||||
[beforeNote.parent_note_id, beforeNote.note_position]);
|
||||
await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId);
|
||||
|
||||
await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId);
|
||||
const now = utils.nowDate();
|
||||
|
||||
const now = utils.nowDate();
|
||||
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
||||
[beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]);
|
||||
|
||||
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
||||
[beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]);
|
||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||
});
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||
});
|
||||
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 afterNoteTreeId = req.params.afterNoteTreeId;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]);
|
||||
const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]);
|
||||
const noteToMove = await getNoteTree(noteTreeId);
|
||||
const afterNote = await getNoteTree(afterNoteTreeId);
|
||||
|
||||
const existing = await getExistingNoteTree(afterNote.parent_note_id, noteToMove.note_id);
|
||||
|
||||
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 validateParentChild(res, afterNote.parent_note_id, noteToMove.note_id, noteTreeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await checkTreeCycle(afterNote.parent_note_id, noteToMove.note_id)) {
|
||||
return res.send({
|
||||
success: false,
|
||||
message: 'Moving note here would create cycle.'
|
||||
});
|
||||
}
|
||||
await sql.doInTransaction(async () => {
|
||||
// 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
|
||||
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0",
|
||||
[afterNote.parent_note_id, afterNote.note_position]);
|
||||
|
||||
if (afterNote) {
|
||||
await sql.doInTransaction(async () => {
|
||||
// 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
|
||||
await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0",
|
||||
[afterNote.parent_note_id, afterNote.note_position]);
|
||||
await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId);
|
||||
|
||||
await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId);
|
||||
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
||||
[afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]);
|
||||
|
||||
await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?",
|
||||
[afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]);
|
||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||
});
|
||||
|
||||
await sync_table.addNoteTreeSync(noteTreeId, sourceId);
|
||||
});
|
||||
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 childNoteId = req.params.childNoteId;
|
||||
const prefix = req.body.prefix;
|
||||
const sourceId = req.headers.source_id;
|
||||
|
||||
const existing = await getExistingNoteTree(parentNoteId, childNoteId);
|
||||
|
||||
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.'
|
||||
});
|
||||
if (!await validateParentChild(res, parentNoteId, childNoteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
}));
|
||||
|
||||
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 afterNoteTreeId = req.params.afterNoteTreeId;
|
||||
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) {
|
||||
return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist.");
|
||||
}
|
||||
|
||||
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.'
|
||||
});
|
||||
if (!await validateParentChild(res, afterNote.parent_note_id, noteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
@@ -237,20 +169,48 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re
|
||||
});
|
||||
|
||||
res.send({ success: true });
|
||||
});
|
||||
}));
|
||||
|
||||
async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
|
||||
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) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!await checkTreeCycleInner(pid)) {
|
||||
@@ -287,7 +247,7 @@ async function checkTreeCycle(parentNoteId, childNoteId) {
|
||||
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 expanded = req.params.expanded;
|
||||
|
||||
@@ -298,6 +258,6 @@ router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res
|
||||
});
|
||||
|
||||
res.send({});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -5,11 +5,12 @@ const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const changePassword = require('../../services/change_password');
|
||||
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);
|
||||
|
||||
res.send(result);
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -4,8 +4,9 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
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(
|
||||
`SELECT
|
||||
notes.is_deleted AS current_is_deleted,
|
||||
@@ -19,6 +20,6 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
||||
LIMIT 1000`);
|
||||
|
||||
res.send(recentChanges);
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -7,12 +7,13 @@ const auth = require('../../services/auth');
|
||||
const utils = require('../../services/utils');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
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());
|
||||
});
|
||||
}));
|
||||
|
||||
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 notePath = req.params.notePath;
|
||||
const sourceId = req.headers.source_id;
|
||||
@@ -31,7 +32,7 @@ router.put('/:noteTreeId/:notePath', auth.checkApiAuth, async (req, res, next) =
|
||||
});
|
||||
|
||||
res.send(await getRecentNotes());
|
||||
});
|
||||
}));
|
||||
|
||||
async function getRecentNotes() {
|
||||
return await sql.getAll(`
|
||||
|
||||
@@ -5,24 +5,25 @@ const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
const options = require('../../services/options');
|
||||
const auth = require('../../services/auth');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
// options allowed to be updated directly in settings dialog
|
||||
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");
|
||||
|
||||
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 ("
|
||||
+ ALLOWED_OPTIONS.map(x => '?').join(",") + ")", ALLOWED_OPTIONS);
|
||||
|
||||
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 sourceId = req.headers.source_id;
|
||||
|
||||
@@ -38,6 +39,6 @@ router.post('/', auth.checkApiAuth, async (req, res, next) => {
|
||||
else {
|
||||
res.send("not allowed option to set");
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -8,8 +8,9 @@ const sql = require('../../services/sql');
|
||||
const utils = require('../../services/utils');
|
||||
const my_scrypt = require('../../services/my_scrypt');
|
||||
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;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
@@ -27,6 +28,6 @@ router.post('', auth.checkAppNotInitialized, async (req, res, next) => {
|
||||
sql.setDbReadyAsResolved();
|
||||
|
||||
res.send({});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -4,8 +4,9 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../services/auth');
|
||||
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;
|
||||
|
||||
try {
|
||||
@@ -20,6 +21,6 @@ router.post('/execute', auth.checkApiAuth, async (req, res, next) => {
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -10,19 +10,20 @@ const sql = require('../../services/sql');
|
||||
const options = require('../../services/options');
|
||||
const content_hash = require('../../services/content_hash');
|
||||
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({
|
||||
'hashes': await content_hash.getHashes(),
|
||||
'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());
|
||||
});
|
||||
}));
|
||||
|
||||
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 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.");
|
||||
|
||||
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 options.setOption('last_synced_pull', 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();
|
||||
|
||||
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;
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
@@ -68,35 +69,35 @@ router.post('/force-note-sync/:noteId', auth.checkApiAuth, async (req, res, next
|
||||
sync.sync();
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
res.send({
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
if (!options.SYNCED_OPTIONS.includes(optName)) {
|
||||
@@ -105,57 +106,86 @@ router.get('/options/:optName', auth.checkApiAuth, async (req, res, next) => {
|
||||
else {
|
||||
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;
|
||||
|
||||
res.send({
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
@@ -10,16 +10,23 @@ const protected_session = require('../../services/protected_session');
|
||||
const data_encryption = require('../../services/data_encryption');
|
||||
const notes = require('../../services/notes');
|
||||
const sync_table = require('../../services/sync_table');
|
||||
const wrap = require('express-promise-wrap').wrap;
|
||||
|
||||
router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
||||
const notes = await sql.getAll("SELECT "
|
||||
+ "notes_tree.*, "
|
||||
+ "notes.note_title, "
|
||||
+ "notes.is_protected "
|
||||
+ "FROM notes_tree "
|
||||
+ "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");
|
||||
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
|
||||
const notes = await sql.getAll(`
|
||||
SELECT
|
||||
notes_tree.*,
|
||||
notes.note_title,
|
||||
notes.is_protected
|
||||
FROM
|
||||
notes_tree
|
||||
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);
|
||||
|
||||
@@ -33,9 +40,9 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
||||
notes: notes,
|
||||
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 isProtected = !!parseInt(req.params.isProtected);
|
||||
const dataKey = protected_session.getDataKey(req);
|
||||
@@ -46,9 +53,9 @@ router.put('/:noteId/protect-sub-tree/:isProtected', auth.checkApiAuth, async (r
|
||||
});
|
||||
|
||||
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 sourceId = req.headers.source_id;
|
||||
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({});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -5,12 +5,13 @@ const router = express.Router();
|
||||
const auth = require('../services/auth');
|
||||
const source_id = require('../services/source_id');
|
||||
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', {
|
||||
sourceId: await source_id.generateSourceId(),
|
||||
maxSyncIdAtLoad: await sql.getFirstValue("SELECT MAX(id) FROM sync")
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -5,12 +5,13 @@ const router = express.Router();
|
||||
const utils = require('../services/utils');
|
||||
const options = require('../services/options');
|
||||
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 });
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('', async (req, res, next) => {
|
||||
router.post('', wrap(async (req, res, next) => {
|
||||
const userName = await options.getOption('username');
|
||||
|
||||
const guessedPassword = req.body.password;
|
||||
@@ -32,7 +33,7 @@ router.post('', async (req, res, next) => {
|
||||
else {
|
||||
res.render('login', {'failedAuth': true});
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
async function verifyPassword(guessed_password) {
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
const express = require('express');
|
||||
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.loggedIn = false;
|
||||
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
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', {});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -24,6 +24,7 @@ const setupApiRoute = require('./api/setup');
|
||||
const sqlRoute = require('./api/sql');
|
||||
const anonymizationRoute = require('./api/anonymization');
|
||||
const cleanupRoute = require('./api/cleanup');
|
||||
const imageRoute = require('./api/image');
|
||||
|
||||
function register(app) {
|
||||
app.use('/', indexRoute);
|
||||
@@ -51,6 +52,7 @@ function register(app) {
|
||||
app.use('/api/sql', sqlRoute);
|
||||
app.use('/api/anonymization', anonymizationRoute);
|
||||
app.use('/api/cleanup', cleanupRoute);
|
||||
app.use('/api/images', imageRoute);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
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', {});
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const build = require('./build');
|
||||
const packageJson = require('../package');
|
||||
|
||||
const APP_DB_VERSION = 61;
|
||||
const APP_DB_VERSION = 65;
|
||||
|
||||
module.exports = {
|
||||
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) {
|
||||
if (!req.session.loggedIn) {
|
||||
res.status(401).send("Not authorized");
|
||||
@@ -63,5 +77,6 @@ module.exports = {
|
||||
checkAuthForMigrationPage,
|
||||
checkApiAuth,
|
||||
checkApiAuthForMigrationPage,
|
||||
checkAppNotInitialized
|
||||
checkAppNotInitialized,
|
||||
checkApiAuthOrElectron
|
||||
};
|
||||
@@ -6,6 +6,7 @@ const fs = require('fs-extra');
|
||||
const dataDir = require('./data_dir');
|
||||
const log = require('./log');
|
||||
const sql = require('./sql');
|
||||
const sync_mutex = require('./sync_mutex');
|
||||
|
||||
async function regularBackup() {
|
||||
const now = new Date();
|
||||
@@ -21,17 +22,25 @@ async function regularBackup() {
|
||||
}
|
||||
|
||||
async function backupNow() {
|
||||
const now = utils.nowDate();
|
||||
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
|
||||
const releaseMutex = await sync_mutex.acquire();
|
||||
|
||||
const backupFile = dataDir.BACKUP_DIR + "/" + "backup-" + utils.getDateTimeForFile() + ".db";
|
||||
try {
|
||||
const now = utils.nowDate();
|
||||
|
||||
fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
|
||||
const backupFile = dataDir.BACKUP_DIR + "/" + "backup-" + utils.getDateTimeForFile() + ".db";
|
||||
|
||||
log.info("Created backup at " + backupFile);
|
||||
fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await options.setOption('last_backup_date', now);
|
||||
});
|
||||
log.info("Created backup at " + backupFile);
|
||||
|
||||
await sql.doInTransaction(async () => {
|
||||
await options.setOption('last_backup_date', now);
|
||||
});
|
||||
}
|
||||
finally {
|
||||
releaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
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-07T21:56:39-05:00", build_revision: "29eb88bac3bbc1db17bb4ed75f9b8bfe16333967" };
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
const sql = require('./sql');
|
||||
const log = require('./log');
|
||||
const messaging = require('./messaging');
|
||||
const sync_mutex = require('./sync_mutex');
|
||||
const utils = require('./utils');
|
||||
|
||||
async function runCheck(query, errorText, errorList) {
|
||||
utils.assertArguments(query, errorText, errorList);
|
||||
|
||||
const result = await sql.getFirstColumn(query);
|
||||
|
||||
if (result.length > 0) {
|
||||
@@ -19,7 +23,7 @@ async function runCheck(query, errorText, errorList) {
|
||||
|
||||
async function checkTreeCycles(errorList) {
|
||||
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) {
|
||||
const childNoteId = row.note_id;
|
||||
@@ -80,11 +84,9 @@ async function runSyncRowChecks(table, key, errorList) {
|
||||
`Missing ${table} records for existing sync rows`, errorList);
|
||||
}
|
||||
|
||||
async function runChecks() {
|
||||
async function runAllChecks() {
|
||||
const errorList = [];
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
note_id
|
||||
@@ -139,7 +141,7 @@ async function runChecks() {
|
||||
WHERE
|
||||
(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
|
||||
`, );
|
||||
`, 'No undeleted note trees for note IDs', errorList);
|
||||
|
||||
await runCheck(`
|
||||
SELECT
|
||||
@@ -161,10 +163,47 @@ async function runChecks() {
|
||||
notes.note_id IS NULL`,
|
||||
"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 runCheck(`
|
||||
SELECT
|
||||
notes_image.note_image_id
|
||||
FROM
|
||||
notes_image
|
||||
JOIN images USING(image_id)
|
||||
WHERE
|
||||
notes_image.is_deleted = 0
|
||||
AND images.is_deleted = 1`,
|
||||
"Note image is not deleted while image is deleted for note_image_id", errorList);
|
||||
|
||||
await runSyncRowChecks("notes", "note_id", errorList);
|
||||
await runSyncRowChecks("notes_history", "note_history_id", errorList);
|
||||
await runSyncRowChecks("notes_tree", "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) {
|
||||
// we run this only if basic checks passed since this assumes basic data consistency
|
||||
@@ -172,7 +211,24 @@ async function runChecks() {
|
||||
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) {
|
||||
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList));
|
||||
|
||||
@@ -19,51 +19,70 @@ async function getHashes() {
|
||||
const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(',');
|
||||
|
||||
const hashes = {
|
||||
notes: getHash(await sql.getAll(`SELECT
|
||||
note_id,
|
||||
note_title,
|
||||
note_text,
|
||||
date_modified,
|
||||
is_protected,
|
||||
is_deleted
|
||||
FROM notes
|
||||
ORDER BY note_id`)),
|
||||
notes: getHash(await sql.getAll(`
|
||||
SELECT
|
||||
note_id,
|
||||
note_title,
|
||||
note_text,
|
||||
date_modified,
|
||||
is_protected,
|
||||
is_deleted
|
||||
FROM notes
|
||||
ORDER BY note_id`)),
|
||||
|
||||
notes_tree: getHash(await sql.getAll(`SELECT
|
||||
note_tree_id,
|
||||
note_id,
|
||||
parent_note_id,
|
||||
note_position,
|
||||
date_modified,
|
||||
is_deleted,
|
||||
prefix
|
||||
FROM notes_tree
|
||||
ORDER BY note_tree_id`)),
|
||||
notes_tree: getHash(await sql.getAll(`
|
||||
SELECT
|
||||
note_tree_id,
|
||||
note_id,
|
||||
parent_note_id,
|
||||
note_position,
|
||||
date_modified,
|
||||
is_deleted,
|
||||
prefix
|
||||
FROM notes_tree
|
||||
ORDER BY note_tree_id`)),
|
||||
|
||||
notes_history: getHash(await sql.getAll(`SELECT
|
||||
note_history_id,
|
||||
note_id,
|
||||
note_title,
|
||||
note_text,
|
||||
date_modified_from,
|
||||
date_modified_to
|
||||
FROM notes_history
|
||||
ORDER BY note_history_id`)),
|
||||
notes_history: getHash(await sql.getAll(`
|
||||
SELECT
|
||||
note_history_id,
|
||||
note_id,
|
||||
note_title,
|
||||
note_text,
|
||||
date_modified_from,
|
||||
date_modified_to
|
||||
FROM notes_history
|
||||
ORDER BY note_history_id`)),
|
||||
|
||||
recent_notes: getHash(await sql.getAll(`SELECT
|
||||
note_tree_id,
|
||||
note_path,
|
||||
date_accessed,
|
||||
is_deleted
|
||||
FROM recent_notes
|
||||
ORDER BY note_path`)),
|
||||
recent_notes: getHash(await sql.getAll(`
|
||||
SELECT
|
||||
note_tree_id,
|
||||
note_path,
|
||||
date_accessed,
|
||||
is_deleted
|
||||
FROM recent_notes
|
||||
ORDER BY note_path`)),
|
||||
|
||||
options: getHash(await sql.getAll(`SELECT
|
||||
opt_name,
|
||||
opt_value
|
||||
FROM options
|
||||
WHERE opt_name IN (${optionsQuestionMarks})
|
||||
ORDER BY opt_name`, options.SYNCED_OPTIONS))
|
||||
options: getHash(await sql.getAll(`
|
||||
SELECT
|
||||
opt_name,
|
||||
opt_value
|
||||
FROM options
|
||||
WHERE opt_name IN (${optionsQuestionMarks})
|
||||
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();
|
||||
|
||||
@@ -74,7 +74,7 @@ async function protectNoteRecursively(noteId, 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) {
|
||||
await protectNoteRecursively(childNoteId, dataKey, protect, sourceId);
|
||||
@@ -135,6 +135,76 @@ async function protectNoteHistory(noteId, dataKey, protect, sourceId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
|
||||
const oldNote = await sql.getFirst("SELECT * FROM notes WHERE note_id = ?", [noteId]);
|
||||
|
||||
if (oldNote.is_protected) {
|
||||
decryptNote(oldNote, dataKey);
|
||||
}
|
||||
|
||||
const newNoteHistoryId = utils.newNoteHistoryId();
|
||||
|
||||
await sql.insert('notes_history', {
|
||||
note_history_id: newNoteHistoryId,
|
||||
note_id: noteId,
|
||||
// title and text should be decrypted now
|
||||
note_title: oldNote.note_title,
|
||||
note_text: oldNote.note_text,
|
||||
is_protected: 0, // will be fixed in the protectNoteHistory() call
|
||||
date_modified_from: oldNote.date_modified,
|
||||
date_modified_to: nowStr
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -154,28 +224,11 @@ async function updateNote(noteId, newNote, dataKey, sourceId) {
|
||||
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]);
|
||||
|
||||
if (oldNote.is_protected) {
|
||||
decryptNote(oldNote, dataKey);
|
||||
}
|
||||
|
||||
const newNoteHistoryId = utils.newNoteHistoryId();
|
||||
|
||||
await sql.insert('notes_history', {
|
||||
note_history_id: newNoteHistoryId,
|
||||
note_id: noteId,
|
||||
// title and text should be decrypted now
|
||||
note_title: oldNote.note_title,
|
||||
note_text: oldNote.note_text,
|
||||
is_protected: 0, // will be fixed in the protectNoteHistory() call
|
||||
date_modified_from: oldNote.date_modified,
|
||||
date_modified_to: nowStr
|
||||
});
|
||||
|
||||
await sync_table.addNoteHistorySync(newNoteHistoryId, sourceId);
|
||||
await saveNoteHistory(noteId, dataKey, sourceId, nowStr);
|
||||
}
|
||||
|
||||
await saveNoteImages(noteId, newNote.detail.note_text, sourceId);
|
||||
|
||||
await protectNoteHistory(noteId, dataKey, newNote.detail.is_protected);
|
||||
|
||||
await sql.execute("UPDATE notes SET note_title = ?, note_text = ?, is_protected = ?, date_modified = ? WHERE note_id = ?", [
|
||||
|
||||
@@ -37,9 +37,9 @@ const dbReady = new Promise((resolve, reject) => {
|
||||
await executeScript(notesSql);
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -14,22 +14,13 @@ const fs = require('fs');
|
||||
const app_info = require('./app_info');
|
||||
const messaging = require('./messaging');
|
||||
const sync_setup = require('./sync_setup');
|
||||
const sync_mutex = require('./sync_mutex');
|
||||
|
||||
let syncInProgress = false;
|
||||
let proxyToggle = true;
|
||||
let syncServerCertificate = null;
|
||||
|
||||
async function sync() {
|
||||
if (syncInProgress) {
|
||||
log.info("Sync already in progress");
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: "Sync already in progress"
|
||||
};
|
||||
}
|
||||
|
||||
syncInProgress = true;
|
||||
const releaseMutex = await sync_mutex.acquire();
|
||||
|
||||
try {
|
||||
if (!await sql.isDbUpToDate()) {
|
||||
@@ -74,7 +65,7 @@ async function sync() {
|
||||
}
|
||||
}
|
||||
finally {
|
||||
syncInProgress = false;
|
||||
releaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +143,12 @@ async function pullSync(syncContext) {
|
||||
else if (sync.entity_name === 'recent_notes') {
|
||||
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 {
|
||||
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') {
|
||||
entity = {
|
||||
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') {
|
||||
@@ -223,6 +220,16 @@ async function pushEntity(sync, syncContext) {
|
||||
else if (sync.entity_name === 'recent_notes') {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
await sql.replace("sync", {
|
||||
entity_name: entityName,
|
||||
@@ -78,6 +86,8 @@ async function fillAllSyncRows() {
|
||||
await fillSyncRows("notes_tree", "note_tree_id");
|
||||
await fillSyncRows("notes_history", "note_history_id");
|
||||
await fillSyncRows("recent_notes", "note_tree_id");
|
||||
await fillSyncRows("images", "image_id");
|
||||
await fillSyncRows("notes_image", "note_image_id");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -87,6 +97,8 @@ module.exports = {
|
||||
addNoteHistorySync,
|
||||
addOptionsSync,
|
||||
addRecentNoteSync,
|
||||
addImageSync,
|
||||
addNoteImageSync,
|
||||
cleanupSyncRowsForMissingEntities,
|
||||
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 = {
|
||||
updateNote,
|
||||
updateNoteTree,
|
||||
updateNoteHistory,
|
||||
updateNoteReordering,
|
||||
updateOptions,
|
||||
updateRecentNotes
|
||||
updateRecentNotes,
|
||||
updateImage,
|
||||
updateNoteImage
|
||||
};
|
||||
@@ -15,6 +15,14 @@ function newNoteHistoryId() {
|
||||
return randomString(12);
|
||||
}
|
||||
|
||||
function newImageId() {
|
||||
return randomString(12);
|
||||
}
|
||||
|
||||
function newNoteImageId() {
|
||||
return randomString(12);
|
||||
}
|
||||
|
||||
function randomString(length) {
|
||||
return randtoken.generate(length);
|
||||
}
|
||||
@@ -79,6 +87,14 @@ function sanitizeSql(str) {
|
||||
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 = {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
@@ -88,6 +104,8 @@ module.exports = {
|
||||
newNoteId,
|
||||
newNoteTreeId,
|
||||
newNoteHistoryId,
|
||||
newImageId,
|
||||
newNoteImageId,
|
||||
toBase64,
|
||||
fromBase64,
|
||||
hmac,
|
||||
@@ -95,5 +113,6 @@ module.exports = {
|
||||
hash,
|
||||
isEmptyOrWhitespace,
|
||||
getDateTimeForFile,
|
||||
sanitizeSql
|
||||
sanitizeSql,
|
||||
assertArguments
|
||||
};
|
||||
@@ -248,12 +248,24 @@
|
||||
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
|
||||
for sharing online for debugging purposes without fear of leaking your personal data.</p>
|
||||
|
||||
<h4>Cleanup</h4>
|
||||
<h4>Image cleanup</h4>
|
||||
|
||||
<button id="cleanup-soft-deleted-items-button" class="btn btn-danger btn-sm">Permanently cleanup soft-deleted items</button> (should be executed in all synced instances)
|
||||
<p>This will remove all image data of images not used in any current version of note from the database (metadata will remain).
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
This means that some images can disappear from note history.</p>
|
||||
|
||||
<button id="cleanup-unused-images-button" class="btn btn-warning btn-sm">Permanently cleanup unused images</button>
|
||||
|
||||
<h4>Soft-delete cleanup</h4>
|
||||
|
||||
<p>This deletes all soft deleted rows from the database. This change isn't synced and should be done manually on all instances.
|
||||
<strong>Use this only if you really know what you're doing.</strong></p>
|
||||
|
||||
<button id="cleanup-soft-deleted-items-button" class="btn btn-danger btn-sm">Permanently cleanup soft-deleted items</button>
|
||||
|
||||
<h4>Vacuum database</h4>
|
||||
|
||||
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
|
||||
|
||||
<button id="vacuum-database-button" class="btn btn-sm">Vacuum database</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user