Compare commits

...

36 Commits

Author SHA1 Message Date
azivner
2dc16dd29f release 0.11.0-beta 2018-04-09 22:38:37 -04:00
azivner
d8924c536b Merge branch 'master' into stable 2018-04-09 22:30:50 -04:00
azivner
3ebbf2cc46 fix generating build.js 2018-04-09 22:30:11 -04:00
azivner
f4079604c9 basic implementation of children overview, closes #80 2018-04-08 22:38:52 -04:00
azivner
1f96a6beab export & import work correctly with clones 2018-04-08 13:14:30 -04:00
azivner
b277a250e5 protected notes are not in autocomplete when not in protected session, fixes #46 2018-04-08 12:27:10 -04:00
azivner
5b0e1a644d codemirror now doesn't hijack alt-left/right, fixes #86 2018-04-08 12:17:42 -04:00
azivner
6bb3cfa9a3 note revisions for code is now properly formatted, fixes #97 2018-04-08 12:13:52 -04:00
azivner
9720868f5a added type and mime to note revisions 2018-04-08 11:57:14 -04:00
azivner
8d8ee2a87a small sync refactorings 2018-04-08 10:09:33 -04:00
azivner
542e82ee5d upgraded uncompressed jquery 2018-04-08 09:40:28 -04:00
azivner
0104b19502 naming standards 2018-04-08 09:25:35 -04:00
azivner
120888b53e fix JSON saving bug 2018-04-08 08:31:19 -04:00
azivner
d2e2caed62 refactoring of note saving code & API 2018-04-08 08:21:49 -04:00
azivner
63066802a8 fix showMessage, showError
(cherry picked from commit 6128bb4)
2018-04-08 07:49:21 -04:00
azivner
6128bb4ff3 fix showMessage, showError 2018-04-08 07:48:47 -04:00
azivner
982796255d sync content check refactoring 2018-04-07 22:59:47 -04:00
azivner
36b15f474d sync cleanup 2018-04-07 22:32:46 -04:00
azivner
13f71f8967 bulk push sync 2018-04-07 22:25:28 -04:00
azivner
64336ffbee implemented bulk sync pull for increased performance 2018-04-07 21:53:42 -04:00
azivner
b09463d1b2 async logging of info messages 2018-04-07 21:30:01 -04:00
azivner
b5e6f46b9c release 0.10.2-beta 2018-04-07 16:07:25 -04:00
azivner
08af4a0465 fix code mirror loading 2018-04-07 15:56:46 -04:00
azivner
8c5df6321f fix windows sqlite binary for electron 2.0 2018-04-07 13:18:08 -04:00
azivner
d19f044961 fix bug 2018-04-07 13:14:01 -04:00
azivner
e378d9f645 label service refactoring + rename of doInTransaction to transactional 2018-04-07 13:03:16 -04:00
azivner
39dc0f71b4 fix execute note 2018-04-06 19:41:48 -04:00
azivner
0cef5c6b8c added showMessage/showError to script api as they are being used 2018-04-06 19:08:42 -04:00
azivner
9b5a44cef4 fix bugs 2018-04-06 18:49:37 -04:00
azivner
29769ed91d fix force note sync 2018-04-06 18:46:29 -04:00
azivner
867d794e17 release 0.10.1-beta 2018-04-06 00:15:04 -04:00
azivner
fdd8458336 fix sync branch route 2018-04-05 23:45:39 -04:00
azivner
a0bec22e96 fix non-200 logging 2018-04-05 23:35:49 -04:00
azivner
5aeb5cd214 jquery upgrade to 3.3.1 2018-04-05 23:18:15 -04:00
azivner
e827ddffb9 electron fixes 2018-04-05 23:17:19 -04:00
azivner
98f80998b9 fix electron build 2018-04-05 19:29:27 -04:00
55 changed files with 1082 additions and 1015 deletions

View File

@@ -24,9 +24,9 @@ jq '.version = "'$VERSION'"' package.json|sponge package.json
git add package.json git add package.json
echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > services/build.js echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > src/services/build.js
git add services/build.js git add src/services/build.js
TAG=v$VERSION TAG=v$VERSION

View File

@@ -0,0 +1,5 @@
ALTER TABLE note_revisions ADD type TEXT DEFAULT '' NOT NULL;
ALTER TABLE note_revisions ADD mime TEXT DEFAULT '' NOT NULL;
UPDATE note_revisions SET type = (SELECT type FROM notes WHERE notes.noteId = note_revisions.noteId);
UPDATE note_revisions SET mime = (SELECT mime FROM notes WHERE notes.noteId = note_revisions.noteId);

View File

@@ -76,12 +76,12 @@ app.on('ready', () => {
const dateNoteService = require('./src/services/date_notes'); const dateNoteService = require('./src/services/date_notes');
const dateUtils = require('./src/services/date_utils'); const dateUtils = require('./src/services/date_utils');
const parentNoteId = await dateNoteService.getDateNoteId(dateUtils.nowDate()); const parentNote = await dateNoteService.getDateNote(dateUtils.nowDate());
// window may be hidden / not in focus // window may be hidden / not in focus
mainWindow.focus(); mainWindow.focus();
mainWindow.webContents.send('create-day-sub-note', parentNoteId); mainWindow.webContents.send('create-day-sub-note', parentNote.noteId);
}); });
if (!result) { if (!result) {

View File

@@ -1,7 +1,7 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.10.0-beta", "version": "0.11.0-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"repository": { "repository": {

View File

@@ -12,7 +12,8 @@ class Note extends Entity {
constructor(row) { constructor(row) {
super(row); super(row);
if (this.isProtected) { // check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
if (this.isProtected && this.noteId) {
protected_session.decryptNote(this); protected_session.decryptNote(this);
} }
@@ -21,6 +22,14 @@ class Note extends Entity {
} }
} }
setContent(content) {
this.content = content;
if (this.isJson()) {
this.jsonContent = JSON.parse(this.content);
}
}
isJson() { isJson() {
return this.mime === "application/json"; return this.mime === "application/json";
} }

View File

@@ -54,7 +54,13 @@ $list.on('change', () => {
const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal); const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal);
$title.html(revisionItem.title); $title.html(revisionItem.title);
$content.html(revisionItem.content);
if (revisionItem.type === 'text') {
$content.html(revisionItem.content);
}
else if (revisionItem.type === 'code') {
$content.html($("<pre>").text(revisionItem.content));
}
}); });
$(document).on('click', "a[action='note-revision']", event => { $(document).on('click', "a[action='note-revision']", event => {

View File

@@ -14,6 +14,10 @@ class Branch {
return await this.treeCache.getNote(this.noteId); return await this.treeCache.getNote(this.noteId);
} }
isTopLevel() {
return this.parentNoteId === 'root';
}
get toString() { get toString() {
return `Branch(branchId=${this.branchId})`; return `Branch(branchId=${this.branchId})`;
} }

View File

@@ -44,6 +44,14 @@ class NoteShort {
get toString() { get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`; return `Note(noteId=${this.noteId}, title=${this.title})`;
} }
get dto() {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.hideInAutocomplete;
return dto;
}
} }
export default NoteShort; export default NoteShort;

View File

@@ -1,20 +1,19 @@
import server from './services/server.js'; import server from './services/server.js';
$(document).ready(() => { $(document).ready(async () => {
server.get('migration').then(result => { const {appDbVersion, dbVersion} = await server.get('migration');
const appDbVersion = result.app_dbVersion;
const dbVersion = result.dbVersion;
if (appDbVersion === dbVersion) { console.log("HI", {appDbVersion, dbVersion});
$("#up-to-date").show();
}
else {
$("#need-to-migrate").show();
$("#app-db-version").html(appDbVersion); if (appDbVersion === dbVersion) {
$("#db-version").html(dbVersion); $("#up-to-date").show();
} }
}); else {
$("#need-to-migrate").show();
$("#app-db-version").html(appDbVersion);
$("#db-version").html(dbVersion);
}
}); });
$("#run-migration").click(async () => { $("#run-migration").click(async () => {
@@ -37,4 +36,11 @@ $("#run-migration").click(async () => {
$("#migration-table").append(row); $("#migration-table").append(row);
} }
});
// copy of this shortcut to be able to debug migration problems
$(document).bind('keydown', 'ctrl+shift+i', () => {
require('electron').remote.getCurrentWindow().toggleDevTools();
return false;
}); });

View File

@@ -1,5 +1,6 @@
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import treeUtils from "./tree_utils.js"; import treeUtils from "./tree_utils.js";
import protectedSessionHolder from './protected_session_holder.js';
async function getAutocompleteItems(parentNoteId, notePath, titlePath) { async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
if (!parentNoteId) { if (!parentNoteId) {
@@ -21,9 +22,6 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
titlePath = ''; titlePath = '';
} }
// https://github.com/zadam/trilium/issues/46
// unfortunately not easy to implement because we don't have an easy access to note's isProtected property
const autocompleteItems = []; const autocompleteItems = [];
for (const childNote of childNotes) { for (const childNote of childNotes) {
@@ -34,10 +32,12 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId; const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId); const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
autocompleteItems.push({ if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
value: childTitlePath + ' (' + childNotePath + ')', autocompleteItems.push({
label: childTitlePath value: childTitlePath + ' (' + childNotePath + ')',
}); label: childTitlePath
});
}
const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath); const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);

View File

@@ -8,6 +8,7 @@ import treeUtils from './tree_utils.js';
import branchPrefixDialog from '../dialogs/branch_prefix.js'; import branchPrefixDialog from '../dialogs/branch_prefix.js';
import infoService from "./info.js"; import infoService from "./info.js";
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import syncService from "./sync.js";
const $tree = $("#tree"); const $tree = $("#tree");
@@ -103,7 +104,7 @@ const contextMenuOptions = {
], ],
beforeOpen: async (event, ui) => { beforeOpen: async (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target); const node = $.ui.fancytree.getNode(ui.target);
const branch = await treeCache.getBranch(branchId); const branch = await treeCache.getBranch(node.data.branchId);
const note = await treeCache.getNote(node.data.noteId); const note = await treeCache.getNote(node.data.noteId);
const parentNote = await treeCache.getNote(branch.parentNoteId); const parentNote = await treeCache.getNote(branch.parentNoteId);

View File

@@ -32,18 +32,19 @@ async function requireLibrary(library) {
} }
} }
const dynamicallyLoadedScripts = []; // we save the promises in case of the same script being required concurrently multiple times
const loadedScriptPromises = {};
async function requireScript(url) { async function requireScript(url) {
if (!dynamicallyLoadedScripts.includes(url)) { if (!loadedScriptPromises[url]) {
dynamicallyLoadedScripts.push(url); loadedScriptPromises[url] = $.ajax({
return await $.ajax({
url: url, url: url,
dataType: "script", dataType: "script",
cache: true cache: true
}) });
} }
await loadedScriptPromises[url];
} }
async function requireCss(url) { async function requireCss(url) {

View File

@@ -100,7 +100,7 @@ setTimeout(() => {
lastSyncId: lastSyncId lastSyncId: lastSyncId
})); }));
}, 1000); }, 1000);
}, 1000); }, 0);
export default { export default {
logError, logError,

View File

@@ -1,4 +1,5 @@
import treeService from './tree.js'; import treeService from './tree.js';
import treeUtils from './tree_utils.js';
import noteTypeService from './note_type.js'; import noteTypeService from './note_type.js';
import protectedSessionService from './protected_session.js'; import protectedSessionService from './protected_session.js';
import protectedSessionHolder from './protected_session_holder.js'; import protectedSessionHolder from './protected_session_holder.js';
@@ -24,6 +25,7 @@ const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display"); const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list"); const $labelList = $("#label-list");
const $labelListInner = $("#label-list-inner"); const $labelListInner = $("#label-list-inner");
const $childrenOverview = $("#children-overview");
let currentNote = null; let currentNote = null;
@@ -73,50 +75,42 @@ function noteChanged() {
async function reload() { async function reload() {
// no saving here // no saving here
await loadNoteToEditor(getCurrentNoteId()); await loadNoteDetail(getCurrentNoteId());
} }
async function switchToNote(noteId) { async function switchToNote(noteId) {
if (getCurrentNoteId() !== noteId) { if (getCurrentNoteId() !== noteId) {
await saveNoteIfChanged(); await saveNoteIfChanged();
await loadNoteToEditor(noteId); await loadNoteDetail(noteId);
} }
} }
async function saveNote() {
const note = getCurrentNote();
note.title = $noteTitle.val();
note.content = getComponent(note.type).getContent();
treeService.setNoteTitle(note.noteId, note.title);
await server.put('notes/' + note.noteId, note.dto);
isNoteChanged = false;
if (note.isProtected) {
protectedSessionHolder.touchProtectedSession();
}
infoService.showMessage("Saved!");
}
async function saveNoteIfChanged() { async function saveNoteIfChanged() {
if (!isNoteChanged) { if (!isNoteChanged) {
return; return;
} }
const note = getCurrentNote(); await saveNote();
updateNoteFromInputs(note);
await saveNoteToServer(note);
if (note.isProtected) {
protectedSessionHolder.touchProtectedSession();
}
}
function updateNoteFromInputs(note) {
note.title = $noteTitle.val();
note.content = getComponent(note.type).getContent();
treeService.setNoteTitle(note.noteId, note.title);
}
async function saveNoteToServer(note) {
const dto = Object.assign({}, note);
delete dto.treeCache;
delete dto.hideInAutocomplete;
await server.put('notes/' + dto.noteId, dto);
isNoteChanged = false;
infoService.showMessage("Saved!");
} }
function setNoteBackgroundIfProtected(note) { function setNoteBackgroundIfProtected(note) {
@@ -145,7 +139,7 @@ async function handleProtectedSession() {
protectedSessionService.ensureDialogIsClosed(); protectedSessionService.ensureDialogIsClosed();
} }
async function loadNoteToEditor(noteId) { async function loadNoteDetail(noteId) {
currentNote = await loadNote(noteId); currentNote = await loadNote(noteId);
if (isNewNoteCreated) { if (isNewNoteCreated) {
@@ -183,6 +177,26 @@ async function loadNoteToEditor(noteId) {
$noteDetailWrapper.scrollTop(0); $noteDetailWrapper.scrollTop(0);
await loadLabelList(); await loadLabelList();
await showChildrenOverview();
}
async function showChildrenOverview() {
const note = getCurrentNote();
$childrenOverview.empty();
const notePath = treeService.getCurrentNotePath();
for (const childBranch of await note.getChildBranches()) {
const link = $('<a>', {
href: 'javascript:',
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
}).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
const childEl = $('<div class="child-overview">').html(link);
$childrenOverview.append(childEl);
}
} }
async function loadLabelList() { async function loadLabelList() {
@@ -245,8 +259,6 @@ setInterval(saveNoteIfChanged, 5000);
export default { export default {
reload, reload,
switchToNote, switchToNote,
updateNoteFromInputs,
saveNoteToServer,
setNoteBackgroundIfProtected, setNoteBackgroundIfProtected,
loadNote, loadNote,
getCurrentNote, getCurrentNote,
@@ -255,6 +267,7 @@ export default {
newNoteCreated, newNoteCreated,
focus, focus,
loadLabelList, loadLabelList,
saveNote,
saveNoteIfChanged, saveNoteIfChanged,
noteChanged noteChanged
}; };

View File

@@ -1,4 +1,3 @@
import utils from "./utils.js";
import libraryLoader from "./library_loader.js"; import libraryLoader from "./library_loader.js";
import bundleService from "./bundle.js"; import bundleService from "./bundle.js";
import infoService from "./info.js"; import infoService from "./info.js";
@@ -11,15 +10,19 @@ const $noteDetailCode = $('#note-detail-code');
const $executeScriptButton = $("#execute-script-button"); const $executeScriptButton = $("#execute-script-button");
async function show() { async function show() {
if (!codeEditor) { await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
if (!codeEditor) {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore"; CodeMirror.keyMap.default["Tab"] = "indentMore";
// these conflict with backward/forward navigation shortcuts
delete CodeMirror.keyMap.default["Alt-Left"];
delete CodeMirror.keyMap.default["Alt-Right"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], { codeEditor = CodeMirror($noteDetailCode[0], {
value: "", value: "",
viewportMargin: Infinity, viewportMargin: Infinity,
indentUnit: 4, indentUnit: 4,
@@ -38,7 +41,7 @@ async function show() {
const currentNote = noteDetailService.getCurrentNote(); const currentNote = noteDetailService.getCurrentNote();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds // this needs to happen after the element is shown, otherwise the editor won't be refreshed
codeEditor.setValue(currentNote.content); codeEditor.setValue(currentNote.content);
const info = CodeMirror.findModeByMIME(currentNote.mime); const info = CodeMirror.findModeByMIME(currentNote.mime);
@@ -67,13 +70,13 @@ async function executeCurrentNote() {
const currentNote = noteDetailService.getCurrentNote(); const currentNote = noteDetailService.getCurrentNote();
if (currentNote.mime.endsWith("env=frontend")) { if (currentNote.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + getCurrentNoteId()); const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
bundleService.executeBundle(bundle); bundleService.executeBundle(bundle);
} }
if (currentNote.mime.endsWith("env=backend")) { if (currentNote.mime.endsWith("env=backend")) {
await server.post('script/run/' + getCurrentNoteId()); await server.post('script/run/' + noteDetailService.getCurrentNoteId());
} }
infoService.showMessage("Note executed"); infoService.showMessage("Note executed");

View File

@@ -1,5 +1,5 @@
import treeService from './tree.js'; import treeService from './tree.js';
import noteDetail from './note_detail.js'; import noteDetailService from './note_detail.js';
import server from './server.js'; import server from './server.js';
import infoService from "./info.js"; import infoService from "./info.js";
@@ -84,13 +84,13 @@ function NoteTypeModel() {
}; };
async function save() { async function save() {
const note = noteDetail.getCurrentNote(); const note = noteDetailService.getCurrentNote();
await server.put('notes/' + note.noteId await server.put('notes/' + note.noteId
+ '/type/' + encodeURIComponent(self.type()) + '/type/' + encodeURIComponent(self.type())
+ '/mime/' + encodeURIComponent(self.mime())); + '/mime/' + encodeURIComponent(self.mime()));
await noteDetail.reload(); await noteDetailService.reload();
// for the note icon to be updated in the tree // for the note icon to be updated in the tree
await treeService.reload(); await treeService.reload();

View File

@@ -1,5 +1,5 @@
import treeService from './tree.js'; import treeService from './tree.js';
import noteDetail from './note_detail.js'; import noteDetailService from './note_detail.js';
import utils from './utils.js'; import utils from './utils.js';
import server from './server.js'; import server from './server.js';
import protectedSessionHolder from './protected_session_holder.js'; import protectedSessionHolder from './protected_session_holder.js';
@@ -57,7 +57,7 @@ async function setupProtectedSession() {
$dialog.dialog("close"); $dialog.dialog("close");
noteDetail.reload(); noteDetailService.reload();
treeService.reload(); treeService.reload();
if (protectedSessionDeferred !== null) { if (protectedSessionDeferred !== null) {
@@ -90,33 +90,27 @@ async function enterProtectedSession(password) {
async function protectNoteAndSendToServer() { async function protectNoteAndSendToServer() {
await ensureProtectedSession(true, true); await ensureProtectedSession(true, true);
const note = noteDetail.getCurrentNote(); const note = noteDetailService.getCurrentNote();
noteDetail.updateNoteFromInputs(note);
note.isProtected = true; note.isProtected = true;
await noteDetail.saveNoteToServer(note); await noteDetailService.saveNote(note);
treeService.setProtected(note.noteId, note.isProtected); treeService.setProtected(note.noteId, note.isProtected);
noteDetail.setNoteBackgroundIfProtected(note); noteDetailService.setNoteBackgroundIfProtected(note);
} }
async function unprotectNoteAndSendToServer() { async function unprotectNoteAndSendToServer() {
await ensureProtectedSession(true, true); await ensureProtectedSession(true, true);
const note = noteDetail.getCurrentNote(); const note = noteDetailService.getCurrentNote();
noteDetail.updateNoteFromInputs(note);
note.isProtected = false; note.isProtected = false;
await noteDetail.saveNoteToServer(note); await noteDetailService.saveNote(note);
treeService.setProtected(note.noteId, note.isProtected); treeService.setProtected(note.noteId, note.isProtected);
noteDetail.setNoteBackgroundIfProtected(note); noteDetailService.setNoteBackgroundIfProtected(note);
} }
async function protectBranch(noteId, protect) { async function protectBranch(noteId, protect) {
@@ -127,7 +121,7 @@ async function protectBranch(noteId, protect) {
infoService.showMessage("Request to un/protect sub tree has finished successfully"); infoService.showMessage("Request to un/protect sub tree has finished successfully");
treeService.reload(); treeService.reload();
noteDetail.reload(); noteDetailService.reload();
} }
$passwordForm.submit(() => { $passwordForm.submit(() => {

View File

@@ -1,6 +1,7 @@
import treeService from './tree.js'; import treeService from './tree.js';
import server from './server.js'; import server from './server.js';
import utils from './utils.js'; import utils from './utils.js';
import infoService from './info.js';
function ScriptApi(startNote, currentNote) { function ScriptApi(startNote, currentNote) {
const $pluginButtons = $("#plugin-buttons"); const $pluginButtons = $("#plugin-buttons");
@@ -54,7 +55,11 @@ function ScriptApi(startNote, currentNote) {
activateNote, activateNote,
getInstanceName: () => window.glob.instanceName, getInstanceName: () => window.glob.instanceName,
runOnServer, runOnServer,
formatDateISO: utils.formatDateISO formatDateISO: utils.formatDateISO,
parseDate: utils.parseDate,
showMessage: infoService.showMessage,
showError: infoService.showError,
reloadTree: treeService.reload
} }
} }

View File

@@ -85,19 +85,17 @@ async function ajax(url, method, data) {
}); });
} }
setTimeout(() => { if (utils.isElectron()) {
if (utils.isElectron()) { const ipc = require('electron').ipcRenderer;
const ipc = require('electron').ipcRenderer;
ipc.on('server-response', (event, arg) => { ipc.on('server-response', (event, arg) => {
console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode); console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
reqResolves[arg.requestId](arg.body); reqResolves[arg.requestId](arg.body);
delete reqResolves[arg.requestId]; delete reqResolves[arg.requestId];
}); });
} }
}, 100);
export default { export default {
get, get,

View File

@@ -1,4 +1,4 @@
import utils from './utils.js'; import server from './server.js';
import infoService from "./info.js"; import infoService from "./info.js";
async function syncNow() { async function syncNow() {
@@ -19,7 +19,7 @@ async function syncNow() {
$("#sync-now-button").click(syncNow); $("#sync-now-button").click(syncNow);
async function forceNoteSync(noteId) { async function forceNoteSync(noteId) {
const result = await server.post('sync/force-note-sync/' + noteId); await server.post('sync/force-note-sync/' + noteId);
infoService.showMessage("Note added to sync queue."); infoService.showMessage("Note added to sync queue.");
} }

View File

@@ -293,7 +293,7 @@ function initFancyTree(branch) {
keyboard: false, // we takover keyboard handling in the hotkeys plugin keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"], extensions: ["hotkeys", "filter", "dnd", "clones"],
source: branch, source: branch,
scrollParent: $("#tree"), scrollParent: $tree,
click: (event, data) => { click: (event, data) => {
const targetType = data.targetType; const targetType = data.targetType;
const node = data.node; const node = data.node;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,9 +5,9 @@
display: grid; display: grid;
grid-template-areas: "header header" grid-template-areas: "header header"
"tree-actions title" "tree-actions title"
"search note-content" "search note-detail"
"tree note-content" "tree note-detail"
"parent-list note-content" "parent-list note-detail"
"parent-list label-list"; "parent-list label-list";
grid-template-columns: 2fr 5fr; grid-template-columns: 2fr 5fr;
grid-template-rows: auto grid-template-rows: auto
@@ -288,4 +288,21 @@ div.ui-tooltip {
#file-table th, #file-table td { #file-table th, #file-table td {
padding: 10px; padding: 10px;
font-size: large; font-size: large;
}
#children-overview {
padding-top: 20px;
}
.child-overview {
font-weight: bold;
font-size: large;
padding: 10px;
border: 1px solid black;
width: 150px;
height: 95px;
margin-right: 20px;
margin-bottom: 20px;
border-radius: 15px;
overflow: hidden;
} }

View File

@@ -13,7 +13,68 @@ async function exportNote(req, res) {
const pack = tar.pack(); const pack = tar.pack();
const name = await exportNoteInner(branchId, '', pack); const exportedNoteIds = [];
const name = await exportNoteInner(branchId, '');
async function exportNoteInner(branchId, directory) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
const childFileName = directory + sanitize(note.title);
if (exportedNoteIds.includes(note.noteId)) {
saveMetadataFile(childFileName, {
version: 1,
clone: true,
noteId: note.noteId,
prefix: branch.prefix
});
return;
}
const metadata = {
version: 1,
clone: false,
noteId: note.noteId,
title: note.title,
prefix: branch.prefix,
type: note.type,
mime: note.mime,
labels: (await note.getLabels()).map(label => {
return {
name: label.name,
value: label.value
};
})
};
if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
return;
}
saveMetadataFile(childFileName, metadata);
saveDataFile(childFileName, note);
exportedNoteIds.push(note.noteId);
for (const child of await note.getChildBranches()) {
await exportNoteInner(child.branchId, childFileName + "/");
}
return childFileName;
}
function saveDataFile(childFileName, note) {
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({name: childFileName + ".dat", size: content.length}, content);
}
function saveMetadataFile(childFileName, metadata) {
const metadataJson = JSON.stringify(metadata, null, '\t');
pack.entry({name: childFileName + ".meta", size: metadataJson.length}, metadataJson);
}
pack.finalize(); pack.finalize();
@@ -23,51 +84,6 @@ async function exportNote(req, res) {
pack.pipe(res); pack.pipe(res);
} }
async function exportNoteInner(branchId, directory, pack) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
if (note.isProtected) {
return;
}
const metadata = await getMetadata(note);
if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
return;
}
const metadataJson = JSON.stringify(metadata, null, '\t');
const childFileName = directory + sanitize(note.title);
pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson);
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({ name: childFileName + ".dat", size: content.length }, content);
for (const child of await note.getChildBranches()) {
await exportNoteInner(child.branchId, childFileName + "/", pack);
}
return childFileName;
}
async function getMetadata(note) {
return {
version: 1,
title: note.title,
type: note.type,
mime: note.mime,
labels: (await note.getLabels()).map(label => {
return {
name: label.name,
value: label.value
};
})
};
}
module.exports = { module.exports = {
exportNote exportNote
}; };

View File

@@ -3,6 +3,7 @@
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const labelService = require('../../services/labels'); const labelService = require('../../services/labels');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const Branch = require('../../entities/branch');
const tar = require('tar-stream'); const tar = require('tar-stream');
const stream = require('stream'); const stream = require('stream');
const path = require('path'); const path = require('path');
@@ -31,7 +32,7 @@ async function parseImportFile(file) {
const extract = tar.extract(); const extract = tar.extract();
extract.on('entry', function(header, stream, next) { extract.on('entry', function(header, stream, next) {
let {name, key} = getFileName(header.name); const {name, key} = getFileName(header.name);
let file = fileMap[name]; let file = fileMap[name];
@@ -97,30 +98,46 @@ async function importTar(req) {
const files = await parseImportFile(file); const files = await parseImportFile(file);
await importNotes(files, parentNoteId); // maps from original noteId (in tar file) to newly generated noteId
const noteIdMap = {};
await importNotes(files, parentNoteId, noteIdMap);
} }
async function importNotes(files, parentNoteId) { async function importNotes(files, parentNoteId, noteIdMap) {
for (const file of files) { for (const file of files) {
if (file.meta.version !== 1) { if (file.meta.version !== 1) {
throw new Error("Can't read meta data version " + file.meta.version); throw new Error("Can't read meta data version " + file.meta.version);
} }
if (file.meta.clone) {
await new Branch({
parentNoteId: parentNoteId,
noteId: noteIdMap[file.meta.noteId],
prefix: file.meta.prefix
}).save();
return;
}
if (file.meta.type !== 'file') { if (file.meta.type !== 'file') {
file.data = file.data.toString("UTF-8"); file.data = file.data.toString("UTF-8");
} }
const {note} = await noteService.createNote(parentNoteId, file.meta.title, file.data, { const {note} = await noteService.createNote(parentNoteId, file.meta.title, file.data, {
type: file.meta.type, type: file.meta.type,
mime: file.meta.mime mime: file.meta.mime,
prefix: file.meta.prefix
}); });
noteIdMap[file.meta.noteId] = note.noteId;
for (const label of file.meta.labels) { for (const label of file.meta.labels) {
await labelService.createLabel(note.noteId, label.name, label.value); await labelService.createLabel(note.noteId, label.name, label.value);
} }
if (file.children.length > 0) { if (file.children.length > 0) {
await importNotes(file.children, note.noteId); await importNotes(file.children, note.noteId, noteIdMap);
} }
} }
} }

View File

@@ -7,7 +7,7 @@ const appInfo = require('../../services/app_info');
async function getMigrationInfo() { async function getMigrationInfo() {
return { return {
dbVersion: parseInt(await optionService.getOption('dbVersion')), dbVersion: parseInt(await optionService.getOption('dbVersion')),
app_dbVersion: appInfo.dbVersion appDbVersion: appInfo.dbVersion
}; };
} }

View File

@@ -36,9 +36,9 @@ async function uploadImage(req) {
return [400, "Unknown image type: " + file.mimetype]; return [400, "Unknown image type: " + file.mimetype];
} }
const parentNoteId = await dateNoteService.getDateNoteId(req.headers['x-local-date']); const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
const {note} = await noteService.createNewNote(parentNoteId, { const {note} = await noteService.createNewNote(parentNote.noteId, {
title: "Sender image", title: "Sender image",
content: "", content: "",
target: 'into', target: 'into',
@@ -57,9 +57,9 @@ async function uploadImage(req) {
} }
async function saveNote(req) { async function saveNote(req) {
const parentNoteId = await dateNoteService.getDateNoteId(req.headers['x-local-date']); const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
await noteService.createNewNote(parentNoteId, { await noteService.createNewNote(parentNote.noteId, {
title: req.body.title, title: req.body.title,
content: req.body.content, content: req.body.content,
target: 'into', target: 'into',

View File

@@ -10,8 +10,8 @@ const log = require('../../services/log');
async function checkSync() { async function checkSync() {
return { return {
'hashes': await contentHashService.getHashes(), hashes: await contentHashService.getHashes(),
'max_sync_id': await sql.getValue('SELECT MAX(id) FROM sync') maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync')
}; };
} }
@@ -55,129 +55,21 @@ async function forceNoteSync(req) {
syncService.sync(); syncService.sync();
} }
async function getChanged() { async function getChanged(req) {
const lastSyncId = parseInt(req.query.lastSyncId); const lastSyncId = parseInt(req.query.lastSyncId);
return await sql.getRows("SELECT * FROM sync WHERE id > ?", [lastSyncId]); const syncs = await sql.getRows("SELECT * FROM sync WHERE id > ? LIMIT 1000", [lastSyncId]);
return await syncService.getSyncRecords(syncs);
} }
async function getNote(req) { async function update(req) {
const noteId = req.params.noteId; const sourceId = req.body.sourceId;
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); const entities = req.body.entities;
syncService.serializeNoteContentBuffer(entity); for (const {sync, entity} of entities) {
await syncUpdateService.updateEntity(sync.entityName, entity, sourceId);
return {
entity: entity
};
}
async function getBranch(req) {
const branchId = req.params.branchId;
return await sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]);
}
async function getNoteRevision(req) {
const noteRevisionId = req.params.noteRevisionId;
return await sql.getRow("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]);
}
async function getOption(req) {
const name = req.params.name;
const opt = await sql.getRow("SELECT * FROM options WHERE name = ?", [name]);
if (!opt.isSynced) {
return [400, "This option can't be synced."];
} }
else {
return opt;
}
}
async function getNoteReordering(req) {
const parentNoteId = req.params.parentNoteId;
return {
parentNoteId: parentNoteId,
ordering: await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId])
};
}
async function getRecentNote(req) {
const branchId = req.params.branchId;
return await sql.getRow("SELECT * FROM recent_notes WHERE branchId = ?", [branchId]);
}
async function getImage(req) {
const imageId = req.params.imageId;
const entity = await sql.getRow("SELECT * FROM images WHERE imageId = ?", [imageId]);
if (entity && entity.data !== null) {
entity.data = entity.data.toString('base64');
}
return entity;
}
async function getNoteImage(req) {
const noteImageId = req.params.noteImageId;
return await sql.getRow("SELECT * FROM note_images WHERE noteImageId = ?", [noteImageId]);
}
async function getLabel(req) {
const labelId = req.params.labelId;
return await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [labelId]);
}
async function getApiToken(req) {
const apiTokenId = req.params.apiTokenId;
return await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [apiTokenId]);
}
async function updateNote(req) {
await syncUpdateService.updateNote(req.body.entity, req.body.sourceId);
}
async function updateBranch(req) {
await syncUpdateService.updateBranch(req.body.entity, req.body.sourceId);
}
async function updateNoteRevision(req) {
await syncUpdateService.updateNoteRevision(req.body.entity, req.body.sourceId);
}
async function updateNoteReordering(req) {
await syncUpdateService.updateNoteReordering(req.body.entity, req.body.sourceId);
}
async function updateOption(req) {
await syncUpdateService.updateOptions(req.body.entity, req.body.sourceId);
}
async function updateRecentNote(req) {
await syncUpdateService.updateRecentNotes(req.body.entity, req.body.sourceId);
}
async function updateImage(req) {
await syncUpdateService.updateImage(req.body.entity, req.body.sourceId);
}
async function updateNoteImage(req) {
await syncUpdateService.updateNoteImage(req.body.entity, req.body.sourceId);
}
async function updateLabel(req) {
await syncUpdateService.updateLabel(req.body.entity, req.body.sourceId);
}
async function updateApiToken(req) {
await syncUpdateService.updateApiToken(req.body.entity, req.body.sourceId);
} }
module.exports = { module.exports = {
@@ -187,24 +79,5 @@ module.exports = {
forceFullSync, forceFullSync,
forceNoteSync, forceNoteSync,
getChanged, getChanged,
getNote, update
getBranch,
getImage,
getNoteImage,
getNoteReordering,
getNoteRevision,
getRecentNote,
getOption,
getLabel,
getApiToken,
updateNote,
updateBranch,
updateImage,
updateNoteImage,
updateNoteReordering,
updateNoteRevision,
updateRecentNote,
updateOption,
updateLabel,
updateApiToken
}; };

View File

@@ -19,6 +19,7 @@ function init(app) {
res.status = function(statusCode) { res.status = function(statusCode) {
res.statusCode = statusCode; res.statusCode = statusCode;
return res;
}; };
res.send = function(obj) { res.send = function(obj) {

View File

@@ -40,22 +40,22 @@ const cls = require('../services/cls');
const sql = require('../services/sql'); const sql = require('../services/sql');
const protectedSessionService = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
function apiResultHandler(res, result) { function apiResultHandler(req, res, result) {
// if it's an array and first element is integer then we consider this to be [statusCode, response] format // if it's an array and first element is integer then we consider this to be [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) { if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result; const [statusCode, response] = result;
res.status(statusCode).send(response); res.status(statusCode).send(response);
if (statusCode !== 200) { if (statusCode !== 200 && statusCode !== 201 && statusCode !== 204) {
log.info(`${method} ${path} returned ${statusCode} with response ${JSON.stringify(response)}`); log.info(`${req.method} ${req.originalUrl} returned ${statusCode} with response ${JSON.stringify(response)}`);
} }
} }
else if (result === undefined) { else if (result === undefined) {
res.status(204).send(); res.status(204).send();
} }
else { else {
res.status(200).send(result); res.send(result);
} }
} }
@@ -70,13 +70,13 @@ function route(method, path, middleware, routeHandler, resultHandler) {
cls.namespace.set('sourceId', req.headers.source_id); cls.namespace.set('sourceId', req.headers.source_id);
protectedSessionService.setProtectedSessionId(req); protectedSessionService.setProtectedSessionId(req);
return await sql.doInTransaction(async () => { return await sql.transactional(async () => {
return await routeHandler(req, res, next); return await routeHandler(req, res, next);
}); });
}); });
if (resultHandler) { if (resultHandler) {
resultHandler(res, result); resultHandler(req, res, result);
} }
} }
catch (e) { catch (e) {
@@ -147,25 +147,7 @@ function register(app) {
apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync);
apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync);
apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged); apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged);
apiRoute(GET, '/api/sync/notes/:noteId', syncApiRoute.getNote); apiRoute(PUT, '/api/sync/update', syncApiRoute.update);
apiRoute(GET, '/api/sync/branches/:branchId', syncApiRoute.getBranch);
apiRoute(GET, '/api/sync/note_revisions/:noteRevisionId', syncApiRoute.getNoteRevision);
apiRoute(GET, '/api/sync/options/:name', syncApiRoute.getOption);
apiRoute(GET, '/api/sync/note_reordering/:parentNoteId', syncApiRoute.getNoteReordering);
apiRoute(GET, '/api/sync/recent_notes/:branchId', syncApiRoute.getRecentNote);
apiRoute(GET, '/api/sync/images/:imageId', syncApiRoute.getImage);
apiRoute(GET, '/api/sync/note_images/:noteImageId', syncApiRoute.getNoteImage);
apiRoute(GET, '/api/sync/labels/:labelId', syncApiRoute.getLabel);
apiRoute(GET, '/api/sync/api_tokens/:apiTokenId', syncApiRoute.getApiToken);
apiRoute(PUT, '/api/sync/notes', syncApiRoute.updateNote);
apiRoute(PUT, '/api/sync/note_revisions', syncApiRoute.updateNoteRevision);
apiRoute(PUT, '/api/sync/note_reordering', syncApiRoute.updateNoteReordering);
apiRoute(PUT, '/api/sync/options', syncApiRoute.updateOption);
apiRoute(PUT, '/api/sync/recent_notes', syncApiRoute.updateRecentNote);
apiRoute(PUT, '/api/sync/images', syncApiRoute.updateImage);
apiRoute(PUT, '/api/sync/note_images', syncApiRoute.updateNoteImage);
apiRoute(PUT, '/api/sync/labels', syncApiRoute.updateLabel);
apiRoute(PUT, '/api/sync/api_tokens', syncApiRoute.updateApiToken);
apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog); apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog);

View File

@@ -3,7 +3,7 @@
const build = require('./build'); const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const APP_DB_VERSION = 86; const APP_DB_VERSION = 87;
module.exports = { module.exports = {
appVersion: packageJson.version, appVersion: packageJson.version,

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-01-17T23:59:03-05:00", buildRevision: "651a9fb3272c85d287c16d5a4978464fb7d2490d" }; module.exports = { buildDate:"2018-04-09T22:38:37-04:00", buildRevision: "d8924c536b70415a9e35299f62bcf978320d8fee" };

View File

@@ -17,7 +17,7 @@ async function changePassword(currentPassword, newPassword) {
const newPasswordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(newPassword)); const newPasswordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(newPassword));
const decryptedDataKey = await passwordEncryptionService.getDataKey(currentPassword); const decryptedDataKey = await passwordEncryptionService.getDataKey(currentPassword);
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await passwordEncryptionService.setDataKey(newPassword, decryptedDataKey); await passwordEncryptionService.setDataKey(newPassword, decryptedDataKey);
await optionService.setOption('passwordVerificationHash', newPasswordVerificationKey); await optionService.setOption('passwordVerificationHash', newPasswordVerificationKey);

View File

@@ -1,6 +1,10 @@
"use strict";
const sql = require('./sql'); const sql = require('./sql');
const utils = require('./utils'); const utils = require('./utils');
const log = require('./log'); const log = require('./log');
const eventLogService = require('./event_log');
const messagingService = require('./messaging');
function getHash(rows) { function getHash(rows) {
let hash = ''; let hash = '';
@@ -121,6 +125,29 @@ async function getHashes() {
return hashes; return hashes;
} }
async function checkContentHashes(otherHashes) {
const hashes = await getHashes();
let allChecksPassed = true;
for (const key in hashes) {
if (hashes[key] !== otherHashes[key]) {
allChecksPassed = false;
await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${resp.hashes[key]}`);
if (key !== 'recent_notes') {
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
await messagingService.sendMessageToAllClients({type: 'sync-hash-check-failed'});
}
}
}
if (allChecksPassed) {
log.info("Content hash checks PASSED");
}
}
module.exports = { module.exports = {
getHashes getHashes,
checkContentHashes
}; };

View File

@@ -4,6 +4,7 @@ const sql = require('./sql');
const noteService = require('./notes'); const noteService = require('./notes');
const labelService = require('./labels'); const labelService = require('./labels');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const repository = require('./repository');
const CALENDAR_ROOT_LABEL = 'calendarRoot'; const CALENDAR_ROOT_LABEL = 'calendarRoot';
const YEAR_LABEL = 'yearNote'; const YEAR_LABEL = 'yearNote';
@@ -14,117 +15,112 @@ const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Satur
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']; const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
async function createNote(parentNoteId, noteTitle, noteText) { async function createNote(parentNoteId, noteTitle, noteText) {
const {note} = await noteService.createNewNote(parentNoteId, { return (await noteService.createNewNote(parentNoteId, {
title: noteTitle, title: noteTitle,
content: noteText, content: noteText,
target: 'into', target: 'into',
isProtected: false isProtected: false
}); })).note;
return note.noteId;
} }
async function getNoteStartingWith(parentNoteId, startsWith) { async function getNoteStartingWith(parentNoteId, startsWith) {
return await sql.getValue(`SELECT noteId FROM notes JOIN branches USING(noteId) return await repository.getEntity(`SELECT notes.* FROM notes JOIN branches USING(noteId)
WHERE parentNoteId = ? AND title LIKE '${startsWith}%' WHERE parentNoteId = ? AND title LIKE '${startsWith}%'
AND notes.isDeleted = 0 AND isProtected = 0 AND notes.isDeleted = 0 AND isProtected = 0
AND branches.isDeleted = 0`, [parentNoteId]); AND branches.isDeleted = 0`, [parentNoteId]);
} }
async function getRootCalendarNoteId() { async function getRootCalendarNote() {
let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN labels USING(noteId) let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
WHERE labels.name = '${CALENDAR_ROOT_LABEL}' AND notes.isDeleted = 0`);
if (!rootNoteId) { if (!rootNote) {
const {rootNote} = await noteService.createNewNote('root', { rootNote = (await noteService.createNewNote('root', {
title: 'Calendar', title: 'Calendar',
target: 'into', target: 'into',
isProtected: false isProtected: false
}); })).note;
const rootNoteId = rootNote.noteId; await labelService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
await labelService.createLabel(rootNoteId, CALENDAR_ROOT_LABEL);
} }
return rootNoteId; return rootNote;
} }
async function getYearNoteId(dateTimeStr, rootNoteId) { async function getYearNote(dateTimeStr, rootNote) {
const yearStr = dateTimeStr.substr(0, 4); const yearStr = dateTimeStr.substr(0, 4);
let yearNoteId = await labelService.getNoteIdWithLabel(YEAR_LABEL, yearStr); let yearNote = await labelService.getNoteWithLabel(YEAR_LABEL, yearStr);
if (!yearNoteId) { if (!yearNote) {
yearNoteId = await getNoteStartingWith(rootNoteId, yearStr); yearNote = await getNoteStartingWith(rootNote.noteId, yearStr);
if (!yearNoteId) { if (!yearNote) {
yearNoteId = await createNote(rootNoteId, yearStr); yearNote = await createNote(rootNote.noteId, yearStr);
} }
await labelService.createLabel(yearNoteId, YEAR_LABEL, yearStr); await labelService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
} }
return yearNoteId; return yearNote;
} }
async function getMonthNoteId(dateTimeStr, rootNoteId) { async function getMonthNote(dateTimeStr, rootNote) {
const monthStr = dateTimeStr.substr(0, 7); const monthStr = dateTimeStr.substr(0, 7);
const monthNumber = dateTimeStr.substr(5, 2); const monthNumber = dateTimeStr.substr(5, 2);
let monthNoteId = await labelService.getNoteIdWithLabel(MONTH_LABEL, monthStr); let monthNote = await labelService.getNoteWithLabel(MONTH_LABEL, monthStr);
if (!monthNoteId) { if (!monthNote) {
const yearNoteId = await getYearNoteId(dateTimeStr, rootNoteId); const yearNote = await getYearNote(dateTimeStr, rootNote);
monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber); monthNote = await getNoteStartingWith(yearNote.noteId, monthNumber);
if (!monthNoteId) { if (!monthNote) {
const dateObj = dateUtils.parseDate(dateTimeStr); const dateObj = dateUtils.parseDate(dateTimeStr);
const noteTitle = monthNumber + " - " + MONTHS[dateObj.getMonth()]; const noteTitle = monthNumber + " - " + MONTHS[dateObj.getMonth()];
monthNoteId = await createNote(yearNoteId, noteTitle); monthNote = await createNote(yearNote.noteId, noteTitle);
} }
await labelService.createLabel(monthNoteId, MONTH_LABEL, monthStr); await labelService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
} }
return monthNoteId; return monthNote;
} }
async function getDateNoteId(dateTimeStr, rootNoteId = null) { async function getDateNote(dateTimeStr, rootNote = null) {
if (!rootNoteId) { if (!rootNote) {
rootNoteId = await getRootCalendarNoteId(); rootNote = await getRootCalendarNote();
} }
const dateStr = dateTimeStr.substr(0, 10); const dateStr = dateTimeStr.substr(0, 10);
const dayNumber = dateTimeStr.substr(8, 2); const dayNumber = dateTimeStr.substr(8, 2);
let dateNoteId = await labelService.getNoteIdWithLabel(DATE_LABEL, dateStr); let dateNote = await labelService.getNoteWithLabel(DATE_LABEL, dateStr);
if (!dateNoteId) { if (!dateNote) {
const monthNoteId = await getMonthNoteId(dateTimeStr, rootNoteId); const monthNote = await getMonthNote(dateTimeStr, rootNote);
dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber); dateNote = await getNoteStartingWith(monthNote.noteId, dayNumber);
if (!dateNoteId) { if (!dateNote) {
const dateObj = dateUtils.parseDate(dateTimeStr); const dateObj = dateUtils.parseDate(dateTimeStr);
const noteTitle = dayNumber + " - " + DAYS[dateObj.getDay()]; const noteTitle = dayNumber + " - " + DAYS[dateObj.getDay()];
dateNoteId = await createNote(monthNoteId, noteTitle); dateNote = await createNote(monthNote.noteId, noteTitle);
} }
await labelService.createLabel(dateNoteId, DATE_LABEL, dateStr); await labelService.createLabel(dateNote.noteId, DATE_LABEL, dateStr);
} }
return dateNoteId; return dateNote;
} }
module.exports = { module.exports = {
getRootCalendarNoteId, getRootCalendarNote,
getYearNoteId, getYearNote,
getMonthNoteId, getMonthNote,
getDateNoteId getDateNote
}; };

View File

@@ -1,6 +1,5 @@
"use strict"; "use strict";
const sql = require('./sql');
const repository = require('./repository'); const repository = require('./repository');
const Label = require('../entities/label'); const Label = require('../entities/label');
@@ -15,14 +14,6 @@ const BUILTIN_LABELS = [
'appCss' 'appCss'
]; ];
async function getNoteIdWithLabel(name, value) {
return await sql.getValue(`SELECT notes.noteId FROM notes JOIN labels USING(noteId)
WHERE notes.isDeleted = 0
AND labels.isDeleted = 0
AND labels.name = ?
AND labels.value = ?`, [name, value]);
}
async function getNotesWithLabel(name, value) { async function getNotesWithLabel(name, value) {
let notes; let notes;
@@ -44,11 +35,6 @@ async function getNoteWithLabel(name, value) {
return notes.length > 0 ? notes[0] : null; return notes.length > 0 ? notes[0] : null;
} }
async function getNoteIdsWithLabel(name) {
return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN labels USING(noteId)
WHERE notes.isDeleted = 0 AND labels.isDeleted = 0 AND labels.name = ? AND labels.isDeleted = 0`, [name]);
}
async function createLabel(noteId, name, value = "") { async function createLabel(noteId, name, value = "") {
return await new Label({ return await new Label({
noteId: noteId, noteId: noteId,
@@ -58,10 +44,8 @@ async function createLabel(noteId, name, value = "") {
} }
module.exports = { module.exports = {
getNoteIdWithLabel,
getNotesWithLabel, getNotesWithLabel,
getNoteWithLabel, getNoteWithLabel,
getNoteIdsWithLabel,
createLabel, createLabel,
BUILTIN_LABELS BUILTIN_LABELS
}; };

View File

@@ -15,14 +15,22 @@ const logger = require('simple-node-logger').createRollingFileLogger({
}); });
function info(message) { function info(message) {
logger.info(message); // info messages are logged asynchronously
setTimeout(() => {
console.log(message);
console.log(message); logger.info(message);
}, 0);
} }
function error(message) { function error(message) {
message = "ERROR: " + message;
// we're using .info() instead of .error() because simple-node-logger emits weird error for showError() // we're using .info() instead of .error() because simple-node-logger emits weird error for showError()
info("ERROR: " + message); // errors are logged synchronously to make sure it doesn't get lost in case of crash
logger.info(message);
console.trace(message);
} }
const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ]; const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ];

View File

@@ -45,7 +45,7 @@ async function migrate() {
// needs to happen outside of the transaction (otherwise it's a NO-OP) // needs to happen outside of the transaction (otherwise it's a NO-OP)
await sql.execute("PRAGMA foreign_keys = OFF"); await sql.execute("PRAGMA foreign_keys = OFF");
await sql.doInTransaction(async () => { await sql.transactional(async () => {
if (mig.type === 'sql') { if (mig.type === 'sql') {
const migrationSql = fs.readFileSync(resourceDir.MIGRATIONS_DIR + "/" + mig.file).toString('utf8'); const migrationSql = fs.readFileSync(resourceDir.MIGRATIONS_DIR + "/" + mig.file).toString('utf8');

View File

@@ -56,6 +56,7 @@ async function createNewNote(parentNoteId, noteData) {
noteId: note.noteId, noteId: note.noteId,
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
notePosition: newNotePos, notePosition: newNotePos,
prefix: noteData.prefix,
isExpanded: 0 isExpanded: 0
}).save(); }).save();
@@ -180,6 +181,8 @@ async function saveNoteRevision(note) {
// title and text should be decrypted now // title and text should be decrypted now
title: note.title, title: note.title,
content: note.content, content: note.content,
type: note.type,
mime: note.mime,
isProtected: 0, // will be fixed in the protectNoteRevisions() call isProtected: 0, // will be fixed in the protectNoteRevisions() call
dateModifiedFrom: note.dateModified, dateModifiedFrom: note.dateModified,
dateModifiedTo: dateUtils.nowDate() dateModifiedTo: dateUtils.nowDate()
@@ -198,7 +201,7 @@ async function updateNote(noteId, noteUpdates) {
await saveNoteRevision(note); await saveNoteRevision(note);
note.title = noteUpdates.title; note.title = noteUpdates.title;
note.content = noteUpdates.content; note.setContent(noteUpdates.content);
note.isProtected = noteUpdates.isProtected; note.isProtected = noteUpdates.isProtected;
await note.save(); await note.save();

View File

@@ -50,7 +50,7 @@ async function updateEntity(entity) {
delete clone.jsonContent; delete clone.jsonContent;
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await sql.replace(entity.constructor.tableName, clone); await sql.replace(entity.constructor.tableName, clone);
const primaryKey = entity[entity.constructor.primaryKeyName]; const primaryKey = entity[entity.constructor.primaryKeyName];

View File

@@ -1,6 +1,7 @@
const scriptService = require('./script'); const scriptService = require('./script');
const repository = require('./repository'); const repository = require('./repository');
const cls = require('./cls'); const cls = require('./cls');
const sqlInit = require('./sql_init');
async function runNotesWithLabel(runAttrValue) { async function runNotesWithLabel(runAttrValue) {
const notes = await repository.getEntities(` const notes = await repository.getEntities(`
@@ -19,8 +20,10 @@ async function runNotesWithLabel(runAttrValue) {
} }
} }
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000); sqlInit.dbReady.then(() => {
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000); setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000); setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
});

View File

@@ -27,7 +27,7 @@ async function executeBundle(bundle, startNote) {
return await execute(ctx, script, ''); return await execute(ctx, script, '');
} }
else { else {
return await sql.doInTransaction(async () => execute(ctx, script, '')); return await sql.transactional(async () => execute(ctx, script, ''));
} }
} }

View File

@@ -56,10 +56,10 @@ function ScriptApi(startNote, currentNote) {
this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`); this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`);
this.getRootCalendarNoteId = dateNoteService.getRootCalendarNoteId; this.getRootCalendarNote = dateNoteService.getRootCalendarNote;
this.getDateNoteId = dateNoteService.getDateNoteId; this.getDateNote = dateNoteService.getDateNote;
this.transactional = sql.doInTransaction; this.transactional = sql.transactional;
} }
module.exports = ScriptContext; module.exports = ScriptContext;

View File

@@ -122,7 +122,7 @@ async function wrap(func) {
let transactionActive = false; let transactionActive = false;
let transactionPromise = null; let transactionPromise = null;
async function doInTransaction(func) { async function transactional(func) {
if (cls.namespace.get('isInTransaction')) { if (cls.namespace.get('isInTransaction')) {
return await func(); return await func();
} }
@@ -181,5 +181,5 @@ module.exports = {
getColumn, getColumn,
execute, execute,
executeScript, executeScript,
doInTransaction transactional
}; };

View File

@@ -58,7 +58,7 @@ async function createInitialDatabase() {
const imagesSql = fs.readFileSync(resourceDir.DB_INIT_DIR + '/main_images.sql', 'UTF-8'); const imagesSql = fs.readFileSync(resourceDir.DB_INIT_DIR + '/main_images.sql', 'UTF-8');
const notesImageSql = fs.readFileSync(resourceDir.DB_INIT_DIR + '/main_note_images.sql', 'UTF-8'); const notesImageSql = fs.readFileSync(resourceDir.DB_INIT_DIR + '/main_note_images.sql', 'UTF-8');
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await sql.executeScript(schema); await sql.executeScript(schema);
await sql.executeScript(notesSql); await sql.executeScript(notesSql);
await sql.executeScript(notesTreeSql); await sql.executeScript(notesTreeSql);

View File

@@ -10,10 +10,8 @@ const sourceIdService = require('./source_id');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update'); const syncUpdateService = require('./sync_update');
const contentHashService = require('./content_hash'); const contentHashService = require('./content_hash');
const eventLogService = require('./event_log');
const fs = require('fs'); const fs = require('fs');
const appInfo = require('./app_info'); const appInfo = require('./app_info');
const messagingService = require('./messaging');
const syncSetup = require('./sync_setup'); const syncSetup = require('./sync_setup');
const syncMutexService = require('./sync_mutex'); const syncMutexService = require('./sync_mutex');
const cls = require('./cls'); const cls = require('./cls');
@@ -91,69 +89,19 @@ async function login() {
return syncContext; return syncContext;
} }
async function getLastSyncedPull() {
return parseInt(await optionService.getOption('lastSyncedPull'));
}
async function setLastSyncedPull(syncId) {
await optionService.setOption('lastSyncedPull', syncId);
}
async function pullSync(syncContext) { async function pullSync(syncContext) {
const lastSyncedPull = await getLastSyncedPull(); const changesUri = '/api/sync/changed?lastSyncId=' + await getLastSyncedPull();
const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull; const rows = await syncRequest(syncContext, 'GET', changesUri);
const syncRows = await syncRequest(syncContext, 'GET', changesUri); log.info("Pulled " + rows.length + " changes from " + changesUri);
log.info("Pulled " + syncRows.length + " changes from " + changesUri); for (const {sync, entity} of rows) {
for (const sync of syncRows) {
if (sourceIdService.isLocalSourceId(sync.sourceId)) { if (sourceIdService.isLocalSourceId(sync.sourceId)) {
log.info(`Skipping pull #${sync.id} ${sync.entityName} ${sync.entityId} because ${sync.sourceId} is a local source id.`); log.info(`Skipping pull #${sync.id} ${sync.entityName} ${sync.entityId} because ${sync.sourceId} is a local source id.`);
await setLastSyncedPull(sync.id);
continue;
}
const resp = await syncRequest(syncContext, 'GET', "/api/sync/" + sync.entityName + "/" + encodeURIComponent(sync.entityId));
if (!resp || (sync.entityName === 'notes' && !resp.entity)) {
log.error(`Empty response to pull for sync #${sync.id} ${sync.entityName}, id=${sync.entityId}`);
}
else if (sync.entityName === 'notes') {
await syncUpdateService.updateNote(resp.entity, syncContext.sourceId);
}
else if (sync.entityName === 'branches') {
await syncUpdateService.updateBranch(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_revisions') {
await syncUpdateService.updateNoteRevision(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_reordering') {
await syncUpdateService.updateNoteReordering(resp, syncContext.sourceId);
}
else if (sync.entityName === 'options') {
await syncUpdateService.updateOptions(resp, syncContext.sourceId);
}
else if (sync.entityName === 'recent_notes') {
await syncUpdateService.updateRecentNotes(resp, syncContext.sourceId);
}
else if (sync.entityName === 'images') {
await syncUpdateService.updateImage(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_images') {
await syncUpdateService.updateNoteImage(resp, syncContext.sourceId);
}
else if (sync.entityName === 'labels') {
await syncUpdateService.updateLabel(resp, syncContext.sourceId);
}
else if (sync.entityName === 'api_tokens') {
await syncUpdateService.updateApiToken(resp, syncContext.sourceId);
} }
else { else {
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); await syncUpdateService.updateEntity(sync.entityName, entity, syncContext.sourceId);
} }
await setLastSyncedPull(sync.id); await setLastSyncedPull(sync.id);
@@ -162,145 +110,69 @@ async function pullSync(syncContext) {
log.info("Finished pull"); log.info("Finished pull");
} }
async function getLastSyncedPush() {
return parseInt(await optionService.getOption('lastSyncedPush'));
}
async function setLastSyncedPush(lastSyncedPush) {
await optionService.setOption('lastSyncedPush', lastSyncedPush);
}
async function pushSync(syncContext) { async function pushSync(syncContext) {
let lastSyncedPush = await getLastSyncedPush(); let lastSyncedPush = await getLastSyncedPush();
while (true) { while (true) {
const sync = await sql.getRowOrNull('SELECT * FROM sync WHERE id > ? LIMIT 1', [lastSyncedPush]); const syncs = await sql.getRows('SELECT * FROM sync WHERE id > ? LIMIT 1000', [lastSyncedPush]);
if (sync === null) { const filteredSyncs = syncs.filter(sync => {
// nothing to sync if (sync.sourceId === syncContext.sourceId) {
log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`);
// this may set lastSyncedPush beyond what's actually sent (because of size limit)
// so this is applied to the database only if there's no actual update
// TODO: it would be better to simplify this somehow
lastSyncedPush = sync.id;
return false;
}
else {
return true;
}
});
if (filteredSyncs.length === 0) {
log.info("Nothing to push"); log.info("Nothing to push");
await setLastSyncedPush(lastSyncedPush);
break; break;
} }
if (sync.sourceId === syncContext.sourceId) { const syncRecords = await getSyncRecords(filteredSyncs);
log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`);
}
else {
await pushEntity(sync, syncContext);
}
lastSyncedPush = sync.id; log.info(`Pushing ${syncRecords.length} syncs.`);
await syncRequest(syncContext, 'PUT', '/api/sync/update', {
sourceId: sourceIdService.getCurrentSourceId(),
entities: syncRecords
});
lastSyncedPush = syncRecords[syncRecords.length - 1].sync.id;
await setLastSyncedPush(lastSyncedPush); await setLastSyncedPush(lastSyncedPush);
} }
} }
async function pushEntity(sync, syncContext) {
let entity;
if (sync.entityName === 'notes') {
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
serializeNoteContentBuffer(entity);
}
else if (sync.entityName === 'branches') {
entity = await sql.getRow('SELECT * FROM branches WHERE branchId = ?', [sync.entityId]);
}
else if (sync.entityName === 'note_revisions') {
entity = await sql.getRow('SELECT * FROM note_revisions WHERE noteRevisionId = ?', [sync.entityId]);
}
else if (sync.entityName === 'note_reordering') {
entity = {
parentNoteId: sync.entityId,
ordering: await sql.getMap('SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [sync.entityId])
};
}
else if (sync.entityName === 'options') {
entity = await sql.getRow('SELECT * FROM options WHERE name = ?', [sync.entityId]);
}
else if (sync.entityName === 'recent_notes') {
entity = await sql.getRow('SELECT * FROM recent_notes WHERE branchId = ?', [sync.entityId]);
}
else if (sync.entityName === 'images') {
entity = await sql.getRow('SELECT * FROM images WHERE imageId = ?', [sync.entityId]);
if (entity.data !== null) {
entity.data = entity.data.toString('base64');
}
}
else if (sync.entityName === 'note_images') {
entity = await sql.getRow('SELECT * FROM note_images WHERE noteImageId = ?', [sync.entityId]);
}
else if (sync.entityName === 'labels') {
entity = await sql.getRow('SELECT * FROM labels WHERE labelId = ?', [sync.entityId]);
}
else if (sync.entityName === 'api_tokens') {
entity = await sql.getRow('SELECT * FROM api_tokens WHERE apiTokenId = ?', [sync.entityId]);
}
else {
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
}
if (!entity) {
log.info(`Sync #${sync.id} entity for ${sync.entityName} ${sync.entityId} doesn't exist. Skipping.`);
return;
}
log.info(`Pushing changes in sync #${sync.id} ${sync.entityName} ${sync.entityId}`);
const payload = {
sourceId: sourceIdService.getCurrentSourceId(),
entity: entity
};
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
}
function serializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = note.content.toString("binary");
}
}
async function checkContentHash(syncContext) { async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
if (await getLastSyncedPull() < resp.max_sync_id) { if (await getLastSyncedPull() < resp.maxSyncId) {
log.info("There are some outstanding pulls, skipping content check."); log.info("There are some outstanding pulls, skipping content check.");
return; return;
} }
const lastSyncedPush = await getLastSyncedPush(); const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [await getLastSyncedPush()]);
const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
if (notPushedSyncs > 0) { if (notPushedSyncs > 0) {
log.info("There's " + notPushedSyncs + " outstanding pushes, skipping content check."); log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`);
return; return;
} }
const hashes = await contentHashService.getHashes(); await contentHashService.checkContentHashes(resp.hashes);
let allChecksPassed = true;
for (const key in hashes) {
if (hashes[key] !== resp.hashes[key]) {
allChecksPassed = false;
await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${resp.hashes[key]}`);
if (key !== 'recent_notes') {
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
await messagingService.sendMessageToAllClients({type: 'sync-hash-check-failed'});
}
}
}
if (allChecksPassed) {
log.info("Content hash checks PASSED");
}
} }
async function syncRequest(syncContext, method, uri, body) { async function syncRequest(syncContext, method, uri, body) {
@@ -331,6 +203,80 @@ async function syncRequest(syncContext, method, uri, body) {
} }
} }
const primaryKeys = {
"notes": "noteId",
"branches": "branchId",
"note_revisions": "noteRevisionId",
"option": "name",
"recent_notes": "branchId",
"images": "imageId",
"note_images": "noteImageId",
"labels": "labelId",
"api_tokens": "apiTokenId"
};
async function getEntityRow(entityName, entityId) {
if (entityName === 'note_reordering') {
return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]);
}
else {
const primaryKey = primaryKeys[entityName];
if (!primaryKey) {
throw new Error("Unknown entity " + entityName);
}
const entity = await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (entityName === 'notes' && entity.type === 'file') {
entity.content = entity.content.toString("binary");
}
else if (entityName === 'images') {
entity.data = entity.data.toString('base64');
}
return entity;
}
}
async function getSyncRecords(syncs) {
const records = [];
let length = 0;
for (const sync of syncs) {
const record = {
sync: sync,
entity: await getEntityRow(sync.entityName, sync.entityId)
};
records.push(record);
length += JSON.stringify(record).length;
if (length > 1000000) {
break;
}
}
return records;
}
async function getLastSyncedPull() {
return parseInt(await optionService.getOption('lastSyncedPull'));
}
async function setLastSyncedPull(syncId) {
await optionService.setOption('lastSyncedPull', syncId);
}
async function getLastSyncedPush() {
return parseInt(await optionService.getOption('lastSyncedPush'));
}
async function setLastSyncedPush(lastSyncedPush) {
await optionService.setOption('lastSyncedPush', lastSyncedPush);
}
sqlInit.dbReady.then(() => { sqlInit.dbReady.then(() => {
if (syncSetup.isSyncSetup) { if (syncSetup.isSyncSetup) {
log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT); log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT);
@@ -357,5 +303,5 @@ sqlInit.dbReady.then(() => {
module.exports = { module.exports = {
sync, sync,
serializeNoteContentBuffer getSyncRecords
}; };

View File

@@ -91,6 +91,8 @@ async function fillSyncRows(entityName, entityKey) {
} }
async function fillAllSyncRows() { async function fillAllSyncRows() {
await sql.execute("DELETE FROM sync");
await fillSyncRows("notes", "noteId"); await fillSyncRows("notes", "noteId");
await fillSyncRows("branches", "branchId"); await fillSyncRows("branches", "branchId");
await fillSyncRows("note_revisions", "noteRevisionId"); await fillSyncRows("note_revisions", "noteRevisionId");

View File

@@ -3,6 +3,42 @@ const log = require('./log');
const eventLogService = require('./event_log'); const eventLogService = require('./event_log');
const syncTableService = require('./sync_table'); const syncTableService = require('./sync_table');
async function updateEntity(entityName, entity, sourceId) {
if (entityName === 'notes') {
await updateNote(entity, sourceId);
}
else if (entityName === 'branches') {
await updateBranch(entity, sourceId);
}
else if (entityName === 'note_revisions') {
await updateNoteRevision(entity, sourceId);
}
else if (entityName === 'note_reordering') {
await updateNoteReordering(entity, sourceId);
}
else if (entityName === 'options') {
await updateOptions(entity, sourceId);
}
else if (entityName === 'recent_notes') {
await updateRecentNotes(entity, sourceId);
}
else if (entityName === 'images') {
await updateImage(entity, sourceId);
}
else if (entityName === 'note_images') {
await updateNoteImage(entity, sourceId);
}
else if (entityName === 'labels') {
await updateLabel(entity, sourceId);
}
else if (entityName === 'api_tokens') {
await updateApiToken(entity, sourceId);
}
else {
throw new Error(`Unrecognized entity type ${entityName}`);
}
}
function deserializeNoteContentBuffer(note) { function deserializeNoteContentBuffer(note) {
if (note.type === 'file') { if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary'); note.content = new Buffer(note.content, 'binary');
@@ -15,7 +51,7 @@ async function updateNote(entity, sourceId) {
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]); const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
if (!origNote || origNote.dateModified <= entity.dateModified) { if (!origNote || origNote.dateModified <= entity.dateModified) {
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await sql.replace("notes", entity); await sql.replace("notes", entity);
await syncTableService.addNoteSync(entity.noteId, sourceId); await syncTableService.addNoteSync(entity.noteId, sourceId);
@@ -29,7 +65,7 @@ async function updateNote(entity, sourceId) {
async function updateBranch(entity, sourceId) { async function updateBranch(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [entity.branchId]); const orig = await sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [entity.branchId]);
await sql.doInTransaction(async () => { await sql.transactional(async () => {
if (orig === null || orig.dateModified < entity.dateModified) { if (orig === null || orig.dateModified < entity.dateModified) {
delete entity.isExpanded; delete entity.isExpanded;
@@ -45,7 +81,7 @@ async function updateBranch(entity, sourceId) {
async function updateNoteRevision(entity, sourceId) { async function updateNoteRevision(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [entity.noteRevisionId]); const orig = await sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [entity.noteRevisionId]);
await sql.doInTransaction(async () => { await sql.transactional(async () => {
// we update note revision even if date modified to is the same because the only thing which might have changed // we update note revision even if date modified to is the same because the only thing which might have changed
// is the protected status (and correnspondingly title and content) which doesn't affect the dateModifiedTo // is the protected status (and correnspondingly title and content) which doesn't affect the dateModifiedTo
if (orig === null || orig.dateModifiedTo <= entity.dateModifiedTo) { if (orig === null || orig.dateModifiedTo <= entity.dateModifiedTo) {
@@ -59,7 +95,7 @@ async function updateNoteRevision(entity, sourceId) {
} }
async function updateNoteReordering(entity, sourceId) { async function updateNoteReordering(entity, sourceId) {
await sql.doInTransaction(async () => { await sql.transactional(async () => {
Object.keys(entity.ordering).forEach(async key => { Object.keys(entity.ordering).forEach(async key => {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity.ordering[key], key]); await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity.ordering[key], key]);
}); });
@@ -75,7 +111,7 @@ async function updateOptions(entity, sourceId) {
return; return;
} }
await sql.doInTransaction(async () => { await sql.transactional(async () => {
if (orig === null || orig.dateModified < entity.dateModified) { if (orig === null || orig.dateModified < entity.dateModified) {
await sql.replace('options', entity); await sql.replace('options', entity);
@@ -90,7 +126,7 @@ async function updateRecentNotes(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE branchId = ?", [entity.branchId]); const orig = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE branchId = ?", [entity.branchId]);
if (orig === null || orig.dateAccessed < entity.dateAccessed) { if (orig === null || orig.dateAccessed < entity.dateAccessed) {
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await sql.replace('recent_notes', entity); await sql.replace('recent_notes', entity);
await syncTableService.addRecentNoteSync(entity.branchId, sourceId); await syncTableService.addRecentNoteSync(entity.branchId, sourceId);
@@ -106,7 +142,7 @@ async function updateImage(entity, sourceId) {
const origImage = await sql.getRow("SELECT * FROM images WHERE imageId = ?", [entity.imageId]); const origImage = await sql.getRow("SELECT * FROM images WHERE imageId = ?", [entity.imageId]);
if (!origImage || origImage.dateModified <= entity.dateModified) { if (!origImage || origImage.dateModified <= entity.dateModified) {
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await sql.replace("images", entity); await sql.replace("images", entity);
await syncTableService.addImageSync(entity.imageId, sourceId); await syncTableService.addImageSync(entity.imageId, sourceId);
@@ -120,7 +156,7 @@ async function updateNoteImage(entity, sourceId) {
const origNoteImage = await sql.getRow("SELECT * FROM note_images WHERE noteImageId = ?", [entity.noteImageId]); const origNoteImage = await sql.getRow("SELECT * FROM note_images WHERE noteImageId = ?", [entity.noteImageId]);
if (!origNoteImage || origNoteImage.dateModified <= entity.dateModified) { if (!origNoteImage || origNoteImage.dateModified <= entity.dateModified) {
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await sql.replace("note_images", entity); await sql.replace("note_images", entity);
await syncTableService.addNoteImageSync(entity.noteImageId, sourceId); await syncTableService.addNoteImageSync(entity.noteImageId, sourceId);
@@ -134,7 +170,7 @@ async function updateLabel(entity, sourceId) {
const origLabel = await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [entity.labelId]); const origLabel = await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [entity.labelId]);
if (!origLabel || origLabel.dateModified <= entity.dateModified) { if (!origLabel || origLabel.dateModified <= entity.dateModified) {
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await sql.replace("labels", entity); await sql.replace("labels", entity);
await syncTableService.addLabelSync(entity.labelId, sourceId); await syncTableService.addLabelSync(entity.labelId, sourceId);
@@ -148,7 +184,7 @@ async function updateApiToken(entity, sourceId) {
const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]); const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]);
if (!apiTokenId) { if (!apiTokenId) {
await sql.doInTransaction(async () => { await sql.transactional(async () => {
await sql.replace("api_tokens", entity); await sql.replace("api_tokens", entity);
await syncTableService.addApiTokenSync(entity.apiTokenId, sourceId); await syncTableService.addApiTokenSync(entity.apiTokenId, sourceId);
@@ -159,14 +195,5 @@ async function updateApiToken(entity, sourceId) {
} }
module.exports = { module.exports = {
updateNote, updateEntity
updateBranch,
updateNoteRevision,
updateNoteReordering,
updateOptions,
updateRecentNotes,
updateImage,
updateNoteImage,
updateLabel,
updateApiToken
}; };

View File

@@ -77,7 +77,7 @@ async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
} }
async function sortNotesAlphabetically(parentNoteId) { async function sortNotesAlphabetically(parentNoteId) {
await sql.doInTransaction(async () => { await sql.transactional(async () => {
const notes = await sql.getRows(`SELECT branchId, noteId, title, isProtected const notes = await sql.getRows(`SELECT branchId, noteId, title, isProtected
FROM notes JOIN branches USING(noteId) FROM notes JOIN branches USING(noteId)
WHERE branches.isDeleted = 0 AND parentNoteId = ?`, [parentNoteId]); WHERE branches.isDeleted = 0 AND parentNoteId = ?`, [parentNoteId]);

View File

@@ -132,76 +132,81 @@
</div> </div>
</div> </div>
<div style="position: relative; overflow: auto; grid-area: note-content; padding-left: 10px; padding-top: 10px;" id="note-detail-wrapper"> <div style="position: relative; overflow: hidden; grid-area: note-detail; padding-left: 10px; padding-top: 10px; display: flex; flex-direction: column;" id="note-detail-wrapper">
<div id="note-detail-text" class="note-detail-component"></div> <div style="flex-grow: 1; position: relative; overflow: auto; flex-basis: content;">
<div id="note-detail-text" style="height: 100%;" class="note-detail-component"></div>
<div id="note-detail-search" class="note-detail-component"> <div id="note-detail-search" class="note-detail-component">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<strong>Search string: &nbsp; &nbsp;</strong> <strong>Search string: &nbsp; &nbsp;</strong>
<textarea rows="4" cols="50" id="search-string"></textarea> <textarea rows="4" cols="50" id="search-string"></textarea>
</div>
<br />
<h4>Help</h4>
<p>
<ul>
<li>
<code>@abc</code> - matches notes with label abc</li>
<li>
<code>@!abc</code> - matches notes without abc label (maybe not the best syntax)</li>
<li>
<code>@abc=true</code> - matches notes with label abc having value true</li>
<li><code>@abc!=true</code></li>
<li>
<code>@"weird label"="weird value"</code> - works also with whitespace inside names values</li>
<li>
<code>@abc and @def</code> - matches notes with both abc and def</li>
<li>
<code>@abc @def</code> - AND relation is implicit when specifying multiple labels</li>
<li>
<code>@abc or @def</code> - OR relation</li>
<li>
<code>@abc&lt;=5</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li>
<code>some search string @abc @def</code> - combination of fulltext and label search - both of them need to match (OR not supported)</li>
<li>
<code>@abc @def some search string</code> - same combination</li>
</ul>
<a href="https://github.com/zadam/trilium/wiki/Labels">Complete help on search syntax</a>
</p>
</div> </div>
<br /> <div id="note-detail-code" class="note-detail-component"></div>
<h4>Help</h4> <div id="note-detail-render" class="note-detail-component"></div>
<p>
<ul>
<li>
<code>@abc</code> - matches notes with label abc</li>
<li>
<code>@!abc</code> - matches notes without abc label (maybe not the best syntax)</li>
<li>
<code>@abc=true</code> - matches notes with label abc having value true</li>
<li><code>@abc!=true</code></li>
<li>
<code>@"weird label"="weird value"</code> - works also with whitespace inside names values</li>
<li>
<code>@abc and @def</code> - matches notes with both abc and def</li>
<li>
<code>@abc @def</code> - AND relation is implicit when specifying multiple labels</li>
<li>
<code>@abc or @def</code> - OR relation</li>
<li>
<code>@abc&lt;=5</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li>
<code>some search string @abc @def</code> - combination of fulltext and label search - both of them need to match (OR not supported)</li>
<li>
<code>@abc @def some search string</code> - same combination</li>
</ul>
<a href="https://github.com/zadam/trilium/wiki/Labels">Complete help on search syntax</a> <div id="note-detail-file" class="note-detail-component">
</p> <table id="file-table">
<tr>
<th>File name:</th>
<td id="file-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="file-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="file-filesize"></td>
</tr>
<tr>
<td>
<button id="file-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="file-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="file-upload" style="display: none" />
</div> </div>
<div id="note-detail-code" class="note-detail-component"></div> <div id="children-overview" style="flex-grow: 1000; flex-shrink: 1000; flex-basis: 1px; height: 100px; overflow: hidden; display: flex; flex-wrap: wrap">
<div id="note-detail-render" class="note-detail-component"></div>
<div id="note-detail-file" class="note-detail-component">
<table id="file-table">
<tr>
<th>File name:</th>
<td id="file-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="file-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="file-filesize"></td>
</tr>
<tr>
<td>
<button id="file-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="file-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div> </div>
<input type="file" id="file-upload" style="display: none" />
</div> </div>
<div id="label-list"> <div id="label-list">

View File

@@ -18,8 +18,6 @@ const log = require('./services/log');
const appInfo = require('./services/app_info'); const appInfo = require('./services/app_info');
const messagingService = require('./services/messaging'); const messagingService = require('./services/messaging');
const utils = require('./services/utils'); const utils = require('./services/utils');
const sql = require('./services/sql');
const sqlInit = require('./services/sql_init');
const port = normalizePort(config['Network']['port'] || '3000'); const port = normalizePort(config['Network']['port'] || '3000');
app.set('port', port); app.set('port', port);
@@ -56,7 +54,7 @@ httpServer.listen(port);
httpServer.on('error', onError); httpServer.on('error', onError);
httpServer.on('listening', onListening); httpServer.on('listening', onListening);
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser)); messagingService.init(httpServer, sessionParser);
if (utils.isElectron()) { if (utils.isElectron()) {
const electronRouting = require('./routes/electron'); const electronRouting = require('./routes/electron');

View File

@@ -5,6 +5,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />