mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-29 01:06:36 +01:00 
			
		
		
		
	Compare commits
	
		
			36 Commits
		
	
	
		
			v0.10.0-be
			...
			v0.11.0-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 2dc16dd29f | ||
|  | d8924c536b | ||
|  | 3ebbf2cc46 | ||
|  | f4079604c9 | ||
|  | 1f96a6beab | ||
|  | b277a250e5 | ||
|  | 5b0e1a644d | ||
|  | 6bb3cfa9a3 | ||
|  | 9720868f5a | ||
|  | 8d8ee2a87a | ||
|  | 542e82ee5d | ||
|  | 0104b19502 | ||
|  | 120888b53e | ||
|  | d2e2caed62 | ||
|  | 63066802a8 | ||
|  | 6128bb4ff3 | ||
|  | 982796255d | ||
|  | 36b15f474d | ||
|  | 13f71f8967 | ||
|  | 64336ffbee | ||
|  | b09463d1b2 | ||
|  | b5e6f46b9c | ||
|  | 08af4a0465 | ||
|  | 8c5df6321f | ||
|  | d19f044961 | ||
|  | e378d9f645 | ||
|  | 39dc0f71b4 | ||
|  | 0cef5c6b8c | ||
|  | 9b5a44cef4 | ||
|  | 29769ed91d | ||
|  | 867d794e17 | ||
|  | fdd8458336 | ||
|  | a0bec22e96 | ||
|  | 5aeb5cd214 | ||
|  | e827ddffb9 | ||
|  | 98f80998b9 | 
| @@ -24,9 +24,9 @@ jq '.version = "'$VERSION'"' package.json|sponge 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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										5
									
								
								db/migrations/0087__add_type_mime_to_note_revision.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrations/0087__add_type_mime_to_note_revision.sql
									
									
									
									
									
										Normal 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); | ||||
| @@ -76,12 +76,12 @@ app.on('ready', () => { | ||||
|         const dateNoteService = require('./src/services/date_notes'); | ||||
|         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 | ||||
|         mainWindow.focus(); | ||||
|  | ||||
|         mainWindow.webContents.send('create-day-sub-note', parentNoteId); | ||||
|         mainWindow.webContents.send('create-day-sub-note', parentNote.noteId); | ||||
|     }); | ||||
|  | ||||
|     if (!result) { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.10.0-beta", | ||||
|   "version": "0.11.0-beta", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "repository": { | ||||
|   | ||||
| @@ -12,7 +12,8 @@ class Note extends Entity { | ||||
|     constructor(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); | ||||
|         } | ||||
|  | ||||
| @@ -21,6 +22,14 @@ class Note extends Entity { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     setContent(content) { | ||||
|         this.content = content; | ||||
|  | ||||
|         if (this.isJson()) { | ||||
|             this.jsonContent = JSON.parse(this.content); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     isJson() { | ||||
|         return this.mime === "application/json"; | ||||
|     } | ||||
|   | ||||
| @@ -54,7 +54,13 @@ $list.on('change', () => { | ||||
|     const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal); | ||||
|  | ||||
|     $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 => { | ||||
|   | ||||
| @@ -14,6 +14,10 @@ class Branch { | ||||
|         return await this.treeCache.getNote(this.noteId); | ||||
|     } | ||||
|  | ||||
|     isTopLevel() { | ||||
|         return this.parentNoteId === 'root'; | ||||
|     } | ||||
|  | ||||
|     get toString() { | ||||
|         return `Branch(branchId=${this.branchId})`; | ||||
|     } | ||||
|   | ||||
| @@ -44,6 +44,14 @@ class NoteShort { | ||||
|     get toString() { | ||||
|         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; | ||||
| @@ -1,20 +1,19 @@ | ||||
| import server from './services/server.js'; | ||||
|  | ||||
| $(document).ready(() => { | ||||
|     server.get('migration').then(result => { | ||||
|         const appDbVersion = result.app_dbVersion; | ||||
|         const dbVersion = result.dbVersion; | ||||
| $(document).ready(async () => { | ||||
|     const {appDbVersion, dbVersion} = await server.get('migration'); | ||||
|  | ||||
|         if (appDbVersion === dbVersion) { | ||||
|             $("#up-to-date").show(); | ||||
|         } | ||||
|         else { | ||||
|             $("#need-to-migrate").show(); | ||||
|     console.log("HI", {appDbVersion, dbVersion}); | ||||
|  | ||||
|             $("#app-db-version").html(appDbVersion); | ||||
|             $("#db-version").html(dbVersion); | ||||
|         } | ||||
|     }); | ||||
|     if (appDbVersion === dbVersion) { | ||||
|         $("#up-to-date").show(); | ||||
|     } | ||||
|     else { | ||||
|         $("#need-to-migrate").show(); | ||||
|  | ||||
|         $("#app-db-version").html(appDbVersion); | ||||
|         $("#db-version").html(dbVersion); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| $("#run-migration").click(async () => { | ||||
| @@ -37,4 +36,11 @@ $("#run-migration").click(async () => { | ||||
|  | ||||
|         $("#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; | ||||
| }); | ||||
| @@ -1,5 +1,6 @@ | ||||
| import treeCache from "./tree_cache.js"; | ||||
| import treeUtils from "./tree_utils.js"; | ||||
| import protectedSessionHolder from './protected_session_holder.js'; | ||||
|  | ||||
| async function getAutocompleteItems(parentNoteId, notePath, titlePath) { | ||||
|     if (!parentNoteId) { | ||||
| @@ -21,9 +22,6 @@ async function getAutocompleteItems(parentNoteId, notePath, 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 = []; | ||||
|  | ||||
|     for (const childNote of childNotes) { | ||||
| @@ -34,10 +32,12 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) { | ||||
|         const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId; | ||||
|         const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId); | ||||
|  | ||||
|         autocompleteItems.push({ | ||||
|             value: childTitlePath + ' (' + childNotePath + ')', | ||||
|             label: childTitlePath | ||||
|         }); | ||||
|         if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) { | ||||
|             autocompleteItems.push({ | ||||
|                 value: childTitlePath + ' (' + childNotePath + ')', | ||||
|                 label: childTitlePath | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath); | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import treeUtils from './tree_utils.js'; | ||||
| import branchPrefixDialog from '../dialogs/branch_prefix.js'; | ||||
| import infoService from "./info.js"; | ||||
| import treeCache from "./tree_cache.js"; | ||||
| import syncService from "./sync.js"; | ||||
|  | ||||
| const $tree = $("#tree"); | ||||
|  | ||||
| @@ -103,7 +104,7 @@ const contextMenuOptions = { | ||||
|     ], | ||||
|     beforeOpen: async (event, ui) => { | ||||
|         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 parentNote = await treeCache.getNote(branch.parentNoteId); | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|     if (!dynamicallyLoadedScripts.includes(url)) { | ||||
|         dynamicallyLoadedScripts.push(url); | ||||
|  | ||||
|         return await $.ajax({ | ||||
|     if (!loadedScriptPromises[url]) { | ||||
|         loadedScriptPromises[url] = $.ajax({ | ||||
|             url: url, | ||||
|             dataType: "script", | ||||
|             cache: true | ||||
|         }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     await loadedScriptPromises[url]; | ||||
| } | ||||
|  | ||||
| async function requireCss(url) { | ||||
|   | ||||
| @@ -100,7 +100,7 @@ setTimeout(() => { | ||||
|             lastSyncId: lastSyncId | ||||
|         })); | ||||
|     }, 1000); | ||||
| }, 1000); | ||||
| }, 0); | ||||
|  | ||||
| export default { | ||||
|     logError, | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import treeService from './tree.js'; | ||||
| import treeUtils from './tree_utils.js'; | ||||
| import noteTypeService from './note_type.js'; | ||||
| import protectedSessionService from './protected_session.js'; | ||||
| import protectedSessionHolder from './protected_session_holder.js'; | ||||
| @@ -24,6 +25,7 @@ const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||
| const $noteIdDisplay = $("#note-id-display"); | ||||
| const $labelList = $("#label-list"); | ||||
| const $labelListInner = $("#label-list-inner"); | ||||
| const $childrenOverview = $("#children-overview"); | ||||
|  | ||||
| let currentNote = null; | ||||
|  | ||||
| @@ -73,50 +75,42 @@ function noteChanged() { | ||||
| async function reload() { | ||||
|     // no saving here | ||||
|  | ||||
|     await loadNoteToEditor(getCurrentNoteId()); | ||||
|     await loadNoteDetail(getCurrentNoteId()); | ||||
| } | ||||
|  | ||||
| async function switchToNote(noteId) { | ||||
|     if (getCurrentNoteId() !== noteId) { | ||||
|         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() { | ||||
|     if (!isNoteChanged) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const note = getCurrentNote(); | ||||
|  | ||||
|     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!"); | ||||
|     await saveNote(); | ||||
| } | ||||
|  | ||||
| function setNoteBackgroundIfProtected(note) { | ||||
| @@ -145,7 +139,7 @@ async function handleProtectedSession() { | ||||
|     protectedSessionService.ensureDialogIsClosed(); | ||||
| } | ||||
|  | ||||
| async function loadNoteToEditor(noteId) { | ||||
| async function loadNoteDetail(noteId) { | ||||
|     currentNote = await loadNote(noteId); | ||||
|  | ||||
|     if (isNewNoteCreated) { | ||||
| @@ -183,6 +177,26 @@ async function loadNoteToEditor(noteId) { | ||||
|     $noteDetailWrapper.scrollTop(0); | ||||
|  | ||||
|     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() { | ||||
| @@ -245,8 +259,6 @@ setInterval(saveNoteIfChanged, 5000); | ||||
| export default { | ||||
|     reload, | ||||
|     switchToNote, | ||||
|     updateNoteFromInputs, | ||||
|     saveNoteToServer, | ||||
|     setNoteBackgroundIfProtected, | ||||
|     loadNote, | ||||
|     getCurrentNote, | ||||
| @@ -255,6 +267,7 @@ export default { | ||||
|     newNoteCreated, | ||||
|     focus, | ||||
|     loadLabelList, | ||||
|     saveNote, | ||||
|     saveNoteIfChanged, | ||||
|     noteChanged | ||||
| }; | ||||
| @@ -1,4 +1,3 @@ | ||||
| import utils from "./utils.js"; | ||||
| import libraryLoader from "./library_loader.js"; | ||||
| import bundleService from "./bundle.js"; | ||||
| import infoService from "./info.js"; | ||||
| @@ -11,15 +10,19 @@ const $noteDetailCode = $('#note-detail-code'); | ||||
| const $executeScriptButton = $("#execute-script-button"); | ||||
|  | ||||
| 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["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'; | ||||
|  | ||||
|         codeEditor = CodeMirror($("#note-detail-code")[0], { | ||||
|         codeEditor = CodeMirror($noteDetailCode[0], { | ||||
|             value: "", | ||||
|             viewportMargin: Infinity, | ||||
|             indentUnit: 4, | ||||
| @@ -38,7 +41,7 @@ async function show() { | ||||
|  | ||||
|     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); | ||||
|  | ||||
|     const info = CodeMirror.findModeByMIME(currentNote.mime); | ||||
| @@ -67,13 +70,13 @@ async function executeCurrentNote() { | ||||
|         const currentNote = noteDetailService.getCurrentNote(); | ||||
|  | ||||
|         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); | ||||
|         } | ||||
|  | ||||
|         if (currentNote.mime.endsWith("env=backend")) { | ||||
|             await server.post('script/run/' + getCurrentNoteId()); | ||||
|             await server.post('script/run/' + noteDetailService.getCurrentNoteId()); | ||||
|         } | ||||
|  | ||||
|         infoService.showMessage("Note executed"); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import treeService from './tree.js'; | ||||
| import noteDetail from './note_detail.js'; | ||||
| import noteDetailService from './note_detail.js'; | ||||
| import server from './server.js'; | ||||
| import infoService from "./info.js"; | ||||
|  | ||||
| @@ -84,13 +84,13 @@ function NoteTypeModel() { | ||||
|     }; | ||||
|  | ||||
|     async function save() { | ||||
|         const note = noteDetail.getCurrentNote(); | ||||
|         const note = noteDetailService.getCurrentNote(); | ||||
|  | ||||
|         await server.put('notes/' + note.noteId | ||||
|             + '/type/' + encodeURIComponent(self.type()) | ||||
|             + '/mime/' + encodeURIComponent(self.mime())); | ||||
|  | ||||
|         await noteDetail.reload(); | ||||
|         await noteDetailService.reload(); | ||||
|  | ||||
|         // for the note icon to be updated in the tree | ||||
|         await treeService.reload(); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import treeService from './tree.js'; | ||||
| import noteDetail from './note_detail.js'; | ||||
| import noteDetailService from './note_detail.js'; | ||||
| import utils from './utils.js'; | ||||
| import server from './server.js'; | ||||
| import protectedSessionHolder from './protected_session_holder.js'; | ||||
| @@ -57,7 +57,7 @@ async function setupProtectedSession() { | ||||
|  | ||||
|     $dialog.dialog("close"); | ||||
|  | ||||
|     noteDetail.reload(); | ||||
|     noteDetailService.reload(); | ||||
|     treeService.reload(); | ||||
|  | ||||
|     if (protectedSessionDeferred !== null) { | ||||
| @@ -90,33 +90,27 @@ async function enterProtectedSession(password) { | ||||
| async function protectNoteAndSendToServer() { | ||||
|     await ensureProtectedSession(true, true); | ||||
|  | ||||
|     const note = noteDetail.getCurrentNote(); | ||||
|  | ||||
|     noteDetail.updateNoteFromInputs(note); | ||||
|  | ||||
|     const note = noteDetailService.getCurrentNote(); | ||||
|     note.isProtected = true; | ||||
|  | ||||
|     await noteDetail.saveNoteToServer(note); | ||||
|     await noteDetailService.saveNote(note); | ||||
|  | ||||
|     treeService.setProtected(note.noteId, note.isProtected); | ||||
|  | ||||
|     noteDetail.setNoteBackgroundIfProtected(note); | ||||
|     noteDetailService.setNoteBackgroundIfProtected(note); | ||||
| } | ||||
|  | ||||
| async function unprotectNoteAndSendToServer() { | ||||
|     await ensureProtectedSession(true, true); | ||||
|  | ||||
|     const note = noteDetail.getCurrentNote(); | ||||
|  | ||||
|     noteDetail.updateNoteFromInputs(note); | ||||
|  | ||||
|     const note = noteDetailService.getCurrentNote(); | ||||
|     note.isProtected = false; | ||||
|  | ||||
|     await noteDetail.saveNoteToServer(note); | ||||
|     await noteDetailService.saveNote(note); | ||||
|  | ||||
|     treeService.setProtected(note.noteId, note.isProtected); | ||||
|  | ||||
|     noteDetail.setNoteBackgroundIfProtected(note); | ||||
|     noteDetailService.setNoteBackgroundIfProtected(note); | ||||
| } | ||||
|  | ||||
| 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"); | ||||
|  | ||||
|     treeService.reload(); | ||||
|     noteDetail.reload(); | ||||
|     noteDetailService.reload(); | ||||
| } | ||||
|  | ||||
| $passwordForm.submit(() => { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import treeService from './tree.js'; | ||||
| import server from './server.js'; | ||||
| import utils from './utils.js'; | ||||
| import infoService from './info.js'; | ||||
|  | ||||
| function ScriptApi(startNote, currentNote) { | ||||
|     const $pluginButtons = $("#plugin-buttons"); | ||||
| @@ -54,7 +55,11 @@ function ScriptApi(startNote, currentNote) { | ||||
|         activateNote, | ||||
|         getInstanceName: () => window.glob.instanceName, | ||||
|         runOnServer, | ||||
|         formatDateISO: utils.formatDateISO | ||||
|         formatDateISO: utils.formatDateISO, | ||||
|         parseDate: utils.parseDate, | ||||
|         showMessage: infoService.showMessage, | ||||
|         showError: infoService.showError, | ||||
|         reloadTree: treeService.reload | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -85,19 +85,17 @@ async function ajax(url, method, data) { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| setTimeout(() => { | ||||
|     if (utils.isElectron()) { | ||||
|         const ipc = require('electron').ipcRenderer; | ||||
| if (utils.isElectron()) { | ||||
|     const ipc = require('electron').ipcRenderer; | ||||
|  | ||||
|         ipc.on('server-response', (event, arg) => { | ||||
|             console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode); | ||||
|     ipc.on('server-response', (event, arg) => { | ||||
|         console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode); | ||||
|  | ||||
|             reqResolves[arg.requestId](arg.body); | ||||
|         reqResolves[arg.requestId](arg.body); | ||||
|  | ||||
|             delete reqResolves[arg.requestId]; | ||||
|         }); | ||||
|     } | ||||
| }, 100); | ||||
|         delete reqResolves[arg.requestId]; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     get, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import utils from './utils.js'; | ||||
| import server from './server.js'; | ||||
| import infoService from "./info.js"; | ||||
|  | ||||
| async function syncNow() { | ||||
| @@ -19,7 +19,7 @@ async function syncNow() { | ||||
| $("#sync-now-button").click(syncNow); | ||||
|  | ||||
| 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."); | ||||
| } | ||||
|   | ||||
| @@ -293,7 +293,7 @@ function initFancyTree(branch) { | ||||
|         keyboard: false, // we takover keyboard handling in the hotkeys plugin | ||||
|         extensions: ["hotkeys", "filter", "dnd", "clones"], | ||||
|         source: branch, | ||||
|         scrollParent: $("#tree"), | ||||
|         scrollParent: $tree, | ||||
|         click: (event, data) => { | ||||
|             const targetType = data.targetType; | ||||
|             const node = data.node; | ||||
|   | ||||
							
								
								
									
										799
									
								
								src/public/libraries/jquery.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										799
									
								
								src/public/libraries/jquery.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										7
									
								
								src/public/libraries/jquery.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								src/public/libraries/jquery.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -5,9 +5,9 @@ | ||||
|     display: grid; | ||||
|     grid-template-areas: "header header" | ||||
|                          "tree-actions title" | ||||
|                          "search note-content" | ||||
|                          "tree note-content" | ||||
|                          "parent-list note-content" | ||||
|                          "search note-detail" | ||||
|                          "tree note-detail" | ||||
|                          "parent-list note-detail" | ||||
|                          "parent-list label-list"; | ||||
|     grid-template-columns: 2fr 5fr; | ||||
|     grid-template-rows: auto | ||||
| @@ -288,4 +288,21 @@ div.ui-tooltip { | ||||
| #file-table th, #file-table td { | ||||
|     padding: 10px; | ||||
|     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; | ||||
| } | ||||
| @@ -13,7 +13,68 @@ async function exportNote(req, res) { | ||||
|  | ||||
|     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(); | ||||
|  | ||||
| @@ -23,51 +84,6 @@ async function exportNote(req, 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 = { | ||||
|     exportNote | ||||
| }; | ||||
| @@ -3,6 +3,7 @@ | ||||
| const repository = require('../../services/repository'); | ||||
| const labelService = require('../../services/labels'); | ||||
| const noteService = require('../../services/notes'); | ||||
| const Branch = require('../../entities/branch'); | ||||
| const tar = require('tar-stream'); | ||||
| const stream = require('stream'); | ||||
| const path = require('path'); | ||||
| @@ -31,7 +32,7 @@ async function parseImportFile(file) { | ||||
|     const extract = tar.extract(); | ||||
|  | ||||
|     extract.on('entry', function(header, stream, next) { | ||||
|         let {name, key} = getFileName(header.name); | ||||
|         const {name, key} = getFileName(header.name); | ||||
|  | ||||
|         let file = fileMap[name]; | ||||
|  | ||||
| @@ -97,30 +98,46 @@ async function importTar(req) { | ||||
|  | ||||
|     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) { | ||||
|         if (file.meta.version !== 1) { | ||||
|             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') { | ||||
|             file.data = file.data.toString("UTF-8"); | ||||
|         } | ||||
|  | ||||
|         const {note} = await noteService.createNote(parentNoteId, file.meta.title, file.data, { | ||||
|             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) { | ||||
|             await labelService.createLabel(note.noteId, label.name, label.value); | ||||
|         } | ||||
|  | ||||
|         if (file.children.length > 0) { | ||||
|             await importNotes(file.children, note.noteId); | ||||
|             await importNotes(file.children, note.noteId, noteIdMap); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,7 @@ const appInfo = require('../../services/app_info'); | ||||
| async function getMigrationInfo() { | ||||
|     return { | ||||
|         dbVersion: parseInt(await optionService.getOption('dbVersion')), | ||||
|         app_dbVersion: appInfo.dbVersion | ||||
|         appDbVersion: appInfo.dbVersion | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -36,9 +36,9 @@ async function uploadImage(req) { | ||||
|         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", | ||||
|         content: "", | ||||
|         target: 'into', | ||||
| @@ -57,9 +57,9 @@ async function uploadImage(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, | ||||
|         content: req.body.content, | ||||
|         target: 'into', | ||||
|   | ||||
| @@ -10,8 +10,8 @@ const log = require('../../services/log'); | ||||
|  | ||||
| async function checkSync() { | ||||
|     return { | ||||
|         'hashes': await contentHashService.getHashes(), | ||||
|         'max_sync_id': await sql.getValue('SELECT MAX(id) FROM sync') | ||||
|         hashes: await contentHashService.getHashes(), | ||||
|         maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync') | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @@ -55,129 +55,21 @@ async function forceNoteSync(req) { | ||||
|     syncService.sync(); | ||||
| } | ||||
|  | ||||
| async function getChanged() { | ||||
| async function getChanged(req) { | ||||
|     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) { | ||||
|     const noteId = req.params.noteId; | ||||
|     const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||
| async function update(req) { | ||||
|     const sourceId = req.body.sourceId; | ||||
|     const entities = req.body.entities; | ||||
|  | ||||
|     syncService.serializeNoteContentBuffer(entity); | ||||
|  | ||||
|     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."]; | ||||
|     for (const {sync, entity} of entities) { | ||||
|         await syncUpdateService.updateEntity(sync.entityName, entity, sourceId); | ||||
|     } | ||||
|     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 = { | ||||
| @@ -187,24 +79,5 @@ module.exports = { | ||||
|     forceFullSync, | ||||
|     forceNoteSync, | ||||
|     getChanged, | ||||
|     getNote, | ||||
|     getBranch, | ||||
|     getImage, | ||||
|     getNoteImage, | ||||
|     getNoteReordering, | ||||
|     getNoteRevision, | ||||
|     getRecentNote, | ||||
|     getOption, | ||||
|     getLabel, | ||||
|     getApiToken, | ||||
|     updateNote, | ||||
|     updateBranch, | ||||
|     updateImage, | ||||
|     updateNoteImage, | ||||
|     updateNoteReordering, | ||||
|     updateNoteRevision, | ||||
|     updateRecentNote, | ||||
|     updateOption, | ||||
|     updateLabel, | ||||
|     updateApiToken | ||||
|     update | ||||
| }; | ||||
| @@ -19,6 +19,7 @@ function init(app) { | ||||
|  | ||||
|         res.status = function(statusCode) { | ||||
|             res.statusCode = statusCode; | ||||
|             return res; | ||||
|         }; | ||||
|  | ||||
|         res.send = function(obj) { | ||||
|   | ||||
| @@ -40,22 +40,22 @@ const cls = require('../services/cls'); | ||||
| const sql = require('../services/sql'); | ||||
| 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 (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) { | ||||
|         const [statusCode, response] = result; | ||||
|  | ||||
|         res.status(statusCode).send(response); | ||||
|  | ||||
|         if (statusCode !== 200) { | ||||
|             log.info(`${method} ${path} returned ${statusCode} with response ${JSON.stringify(response)}`); | ||||
|         if (statusCode !== 200 && statusCode !== 201 && statusCode !== 204) { | ||||
|             log.info(`${req.method} ${req.originalUrl} returned ${statusCode} with response ${JSON.stringify(response)}`); | ||||
|         } | ||||
|     } | ||||
|     else if (result === undefined) { | ||||
|         res.status(204).send(); | ||||
|     } | ||||
|     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); | ||||
|                 protectedSessionService.setProtectedSessionId(req); | ||||
|  | ||||
|                 return await sql.doInTransaction(async () => { | ||||
|                 return await sql.transactional(async () => { | ||||
|                     return await routeHandler(req, res, next); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             if (resultHandler) { | ||||
|                 resultHandler(res, result); | ||||
|                 resultHandler(req, res, result); | ||||
|             } | ||||
|         } | ||||
|         catch (e) { | ||||
| @@ -147,25 +147,7 @@ function register(app) { | ||||
|     apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); | ||||
|     apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); | ||||
|     apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged); | ||||
|     apiRoute(GET, '/api/sync/notes/:noteId', syncApiRoute.getNote); | ||||
|     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(PUT, '/api/sync/update', syncApiRoute.update); | ||||
|  | ||||
|     apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog); | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
|  | ||||
| const APP_DB_VERSION = 86; | ||||
| const APP_DB_VERSION = 87; | ||||
|  | ||||
| module.exports = { | ||||
|     appVersion: packageJson.version, | ||||
|   | ||||
| @@ -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" }; | ||||
|   | ||||
| @@ -17,7 +17,7 @@ async function changePassword(currentPassword, newPassword) { | ||||
|     const newPasswordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(newPassword)); | ||||
|     const decryptedDataKey = await passwordEncryptionService.getDataKey(currentPassword); | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|     await sql.transactional(async () => { | ||||
|         await passwordEncryptionService.setDataKey(newPassword, decryptedDataKey); | ||||
|  | ||||
|         await optionService.setOption('passwordVerificationHash', newPasswordVerificationKey); | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sql = require('./sql'); | ||||
| const utils = require('./utils'); | ||||
| const log = require('./log'); | ||||
| const eventLogService = require('./event_log'); | ||||
| const messagingService = require('./messaging'); | ||||
|  | ||||
| function getHash(rows) { | ||||
|     let hash = ''; | ||||
| @@ -121,6 +125,29 @@ async function getHashes() { | ||||
|     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 = { | ||||
|     getHashes | ||||
|     getHashes, | ||||
|     checkContentHashes | ||||
| }; | ||||
| @@ -4,6 +4,7 @@ const sql = require('./sql'); | ||||
| const noteService = require('./notes'); | ||||
| const labelService = require('./labels'); | ||||
| const dateUtils = require('./date_utils'); | ||||
| const repository = require('./repository'); | ||||
|  | ||||
| const CALENDAR_ROOT_LABEL = 'calendarRoot'; | ||||
| 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']; | ||||
|  | ||||
| async function createNote(parentNoteId, noteTitle, noteText) { | ||||
|     const {note} = await noteService.createNewNote(parentNoteId, { | ||||
|     return (await noteService.createNewNote(parentNoteId, { | ||||
|         title: noteTitle, | ||||
|         content: noteText, | ||||
|         target: 'into', | ||||
|         isProtected: false | ||||
|     }); | ||||
|  | ||||
|     return note.noteId; | ||||
|     })).note; | ||||
| } | ||||
|  | ||||
| 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}%' | ||||
|                                     AND notes.isDeleted = 0 AND isProtected = 0  | ||||
|                                     AND branches.isDeleted = 0`, [parentNoteId]); | ||||
| } | ||||
|  | ||||
| async function getRootCalendarNoteId() { | ||||
|     let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN labels USING(noteId)  | ||||
|               WHERE labels.name = '${CALENDAR_ROOT_LABEL}' AND notes.isDeleted = 0`); | ||||
| async function getRootCalendarNote() { | ||||
|     let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL); | ||||
|  | ||||
|     if (!rootNoteId) { | ||||
|         const {rootNote} = await noteService.createNewNote('root', { | ||||
|     if (!rootNote) { | ||||
|         rootNote = (await noteService.createNewNote('root', { | ||||
|             title: 'Calendar', | ||||
|             target: 'into', | ||||
|             isProtected: false | ||||
|         }); | ||||
|         })).note; | ||||
|  | ||||
|         const rootNoteId = rootNote.noteId; | ||||
|  | ||||
|         await labelService.createLabel(rootNoteId, CALENDAR_ROOT_LABEL); | ||||
|         await labelService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL); | ||||
|     } | ||||
|  | ||||
|     return rootNoteId; | ||||
|     return rootNote; | ||||
| } | ||||
|  | ||||
| async function getYearNoteId(dateTimeStr, rootNoteId) { | ||||
| async function getYearNote(dateTimeStr, rootNote) { | ||||
|     const yearStr = dateTimeStr.substr(0, 4); | ||||
|  | ||||
|     let yearNoteId = await labelService.getNoteIdWithLabel(YEAR_LABEL, yearStr); | ||||
|     let yearNote = await labelService.getNoteWithLabel(YEAR_LABEL, yearStr); | ||||
|  | ||||
|     if (!yearNoteId) { | ||||
|         yearNoteId = await getNoteStartingWith(rootNoteId, yearStr); | ||||
|     if (!yearNote) { | ||||
|         yearNote = await getNoteStartingWith(rootNote.noteId, yearStr); | ||||
|  | ||||
|         if (!yearNoteId) { | ||||
|             yearNoteId = await createNote(rootNoteId, yearStr); | ||||
|         if (!yearNote) { | ||||
|             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 monthNumber = dateTimeStr.substr(5, 2); | ||||
|  | ||||
|     let monthNoteId = await labelService.getNoteIdWithLabel(MONTH_LABEL, monthStr); | ||||
|     let monthNote = await labelService.getNoteWithLabel(MONTH_LABEL, monthStr); | ||||
|  | ||||
|     if (!monthNoteId) { | ||||
|         const yearNoteId = await getYearNoteId(dateTimeStr, rootNoteId); | ||||
|     if (!monthNote) { | ||||
|         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 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) { | ||||
|     if (!rootNoteId) { | ||||
|         rootNoteId = await getRootCalendarNoteId(); | ||||
| async function getDateNote(dateTimeStr, rootNote = null) { | ||||
|     if (!rootNote) { | ||||
|         rootNote = await getRootCalendarNote(); | ||||
|     } | ||||
|  | ||||
|     const dateStr = dateTimeStr.substr(0, 10); | ||||
|     const dayNumber = dateTimeStr.substr(8, 2); | ||||
|  | ||||
|     let dateNoteId = await labelService.getNoteIdWithLabel(DATE_LABEL, dateStr); | ||||
|     let dateNote = await labelService.getNoteWithLabel(DATE_LABEL, dateStr); | ||||
|  | ||||
|     if (!dateNoteId) { | ||||
|         const monthNoteId = await getMonthNoteId(dateTimeStr, rootNoteId); | ||||
|     if (!dateNote) { | ||||
|         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 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 = { | ||||
|     getRootCalendarNoteId, | ||||
|     getYearNoteId, | ||||
|     getMonthNoteId, | ||||
|     getDateNoteId | ||||
|     getRootCalendarNote, | ||||
|     getYearNote, | ||||
|     getMonthNote, | ||||
|     getDateNote | ||||
| }; | ||||
| @@ -1,6 +1,5 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sql = require('./sql'); | ||||
| const repository = require('./repository'); | ||||
| const Label = require('../entities/label'); | ||||
|  | ||||
| @@ -15,14 +14,6 @@ const BUILTIN_LABELS = [ | ||||
|     '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) { | ||||
|     let notes; | ||||
|  | ||||
| @@ -44,11 +35,6 @@ async function getNoteWithLabel(name, value) { | ||||
|     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 = "") { | ||||
|     return await new Label({ | ||||
|         noteId: noteId, | ||||
| @@ -58,10 +44,8 @@ async function createLabel(noteId, name, value = "") { | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     getNoteIdWithLabel, | ||||
|     getNotesWithLabel, | ||||
|     getNoteWithLabel, | ||||
|     getNoteIdsWithLabel, | ||||
|     createLabel, | ||||
|     BUILTIN_LABELS | ||||
| }; | ||||
| @@ -15,14 +15,22 @@ const logger = require('simple-node-logger').createRollingFileLogger({ | ||||
| }); | ||||
|  | ||||
| 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) { | ||||
|     message = "ERROR: " + message; | ||||
|  | ||||
|     // 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" ]; | ||||
|   | ||||
| @@ -45,7 +45,7 @@ async function migrate() { | ||||
|             // needs to happen outside of the transaction (otherwise it's a NO-OP) | ||||
|             await sql.execute("PRAGMA foreign_keys = OFF"); | ||||
|  | ||||
|             await sql.doInTransaction(async () => { | ||||
|             await sql.transactional(async () => { | ||||
|                 if (mig.type === 'sql') { | ||||
|                     const migrationSql = fs.readFileSync(resourceDir.MIGRATIONS_DIR + "/" + mig.file).toString('utf8'); | ||||
|  | ||||
|   | ||||
| @@ -56,6 +56,7 @@ async function createNewNote(parentNoteId, noteData) { | ||||
|         noteId: note.noteId, | ||||
|         parentNoteId: parentNoteId, | ||||
|         notePosition: newNotePos, | ||||
|         prefix: noteData.prefix, | ||||
|         isExpanded: 0 | ||||
|     }).save(); | ||||
|  | ||||
| @@ -180,6 +181,8 @@ async function saveNoteRevision(note) { | ||||
|             // title and text should be decrypted now | ||||
|             title: note.title, | ||||
|             content: note.content, | ||||
|             type: note.type, | ||||
|             mime: note.mime, | ||||
|             isProtected: 0, // will be fixed in the protectNoteRevisions() call | ||||
|             dateModifiedFrom: note.dateModified, | ||||
|             dateModifiedTo: dateUtils.nowDate() | ||||
| @@ -198,7 +201,7 @@ async function updateNote(noteId, noteUpdates) { | ||||
|     await saveNoteRevision(note); | ||||
|  | ||||
|     note.title = noteUpdates.title; | ||||
|     note.content = noteUpdates.content; | ||||
|     note.setContent(noteUpdates.content); | ||||
|     note.isProtected = noteUpdates.isProtected; | ||||
|     await note.save(); | ||||
|  | ||||
|   | ||||
| @@ -50,7 +50,7 @@ async function updateEntity(entity) { | ||||
|  | ||||
|     delete clone.jsonContent; | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|     await sql.transactional(async () => { | ||||
|         await sql.replace(entity.constructor.tableName, clone); | ||||
|  | ||||
|         const primaryKey = entity[entity.constructor.primaryKeyName]; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| const scriptService = require('./script'); | ||||
| const repository = require('./repository'); | ||||
| const cls = require('./cls'); | ||||
| const sqlInit = require('./sql_init'); | ||||
|  | ||||
| async function runNotesWithLabel(runAttrValue) { | ||||
|     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); | ||||
| }); | ||||
| @@ -27,7 +27,7 @@ async function executeBundle(bundle, startNote) { | ||||
|         return await execute(ctx, script, ''); | ||||
|     } | ||||
|     else { | ||||
|         return await sql.doInTransaction(async () => execute(ctx, script, '')); | ||||
|         return await sql.transactional(async () => execute(ctx, script, '')); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -56,10 +56,10 @@ function ScriptApi(startNote, currentNote) { | ||||
|  | ||||
|     this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`); | ||||
|  | ||||
|     this.getRootCalendarNoteId = dateNoteService.getRootCalendarNoteId; | ||||
|     this.getDateNoteId = dateNoteService.getDateNoteId; | ||||
|     this.getRootCalendarNote = dateNoteService.getRootCalendarNote; | ||||
|     this.getDateNote = dateNoteService.getDateNote; | ||||
|  | ||||
|     this.transactional = sql.doInTransaction; | ||||
|     this.transactional = sql.transactional; | ||||
| } | ||||
|  | ||||
| module.exports = ScriptContext; | ||||
| @@ -122,7 +122,7 @@ async function wrap(func) { | ||||
| let transactionActive = false; | ||||
| let transactionPromise = null; | ||||
|  | ||||
| async function doInTransaction(func) { | ||||
| async function transactional(func) { | ||||
|     if (cls.namespace.get('isInTransaction')) { | ||||
|         return await func(); | ||||
|     } | ||||
| @@ -181,5 +181,5 @@ module.exports = { | ||||
|     getColumn, | ||||
|     execute, | ||||
|     executeScript, | ||||
|     doInTransaction | ||||
|     transactional | ||||
| }; | ||||
| @@ -58,7 +58,7 @@ async function createInitialDatabase() { | ||||
|     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'); | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|     await sql.transactional(async () => { | ||||
|         await sql.executeScript(schema); | ||||
|         await sql.executeScript(notesSql); | ||||
|         await sql.executeScript(notesTreeSql); | ||||
|   | ||||
| @@ -10,10 +10,8 @@ const sourceIdService = require('./source_id'); | ||||
| const dateUtils = require('./date_utils'); | ||||
| const syncUpdateService = require('./sync_update'); | ||||
| const contentHashService = require('./content_hash'); | ||||
| const eventLogService = require('./event_log'); | ||||
| const fs = require('fs'); | ||||
| const appInfo = require('./app_info'); | ||||
| const messagingService = require('./messaging'); | ||||
| const syncSetup = require('./sync_setup'); | ||||
| const syncMutexService = require('./sync_mutex'); | ||||
| const cls = require('./cls'); | ||||
| @@ -91,69 +89,19 @@ async function login() { | ||||
|     return syncContext; | ||||
| } | ||||
|  | ||||
| async function getLastSyncedPull() { | ||||
|     return parseInt(await optionService.getOption('lastSyncedPull')); | ||||
| } | ||||
|  | ||||
| async function setLastSyncedPull(syncId) { | ||||
|     await optionService.setOption('lastSyncedPull', syncId); | ||||
| } | ||||
|  | ||||
| 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 of syncRows) { | ||||
|     for (const {sync, entity} of rows) { | ||||
|         if (sourceIdService.isLocalSourceId(sync.sourceId)) { | ||||
|             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 { | ||||
|             throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); | ||||
|             await syncUpdateService.updateEntity(sync.entityName, entity, syncContext.sourceId); | ||||
|         } | ||||
|  | ||||
|         await setLastSyncedPull(sync.id); | ||||
| @@ -162,145 +110,69 @@ async function pullSync(syncContext) { | ||||
|     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) { | ||||
|     let lastSyncedPush = await getLastSyncedPush(); | ||||
|  | ||||
|     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) { | ||||
|             // nothing to sync | ||||
|         const filteredSyncs = syncs.filter(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"); | ||||
|  | ||||
|             await setLastSyncedPush(lastSyncedPush); | ||||
|  | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         if (sync.sourceId === syncContext.sourceId) { | ||||
|             log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`); | ||||
|         } | ||||
|         else { | ||||
|             await pushEntity(sync, syncContext); | ||||
|         } | ||||
|         const syncRecords = await getSyncRecords(filteredSyncs); | ||||
|  | ||||
|         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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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) { | ||||
|     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."); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const lastSyncedPush = await getLastSyncedPush(); | ||||
|     const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]); | ||||
|     const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [await getLastSyncedPush()]); | ||||
|  | ||||
|     if (notPushedSyncs > 0) { | ||||
|         log.info("There's " + notPushedSyncs + " outstanding pushes, skipping content check."); | ||||
|         log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const hashes = await contentHashService.getHashes(); | ||||
|     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"); | ||||
|     } | ||||
|     await contentHashService.checkContentHashes(resp.hashes); | ||||
| } | ||||
|  | ||||
| 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(() => { | ||||
|     if (syncSetup.isSyncSetup) { | ||||
|         log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT); | ||||
| @@ -357,5 +303,5 @@ sqlInit.dbReady.then(() => { | ||||
|  | ||||
| module.exports = { | ||||
|     sync, | ||||
|     serializeNoteContentBuffer | ||||
|     getSyncRecords | ||||
| }; | ||||
| @@ -91,6 +91,8 @@ async function fillSyncRows(entityName, entityKey) { | ||||
| } | ||||
|  | ||||
| async function fillAllSyncRows() { | ||||
|     await sql.execute("DELETE FROM sync"); | ||||
|  | ||||
|     await fillSyncRows("notes", "noteId"); | ||||
|     await fillSyncRows("branches", "branchId"); | ||||
|     await fillSyncRows("note_revisions", "noteRevisionId"); | ||||
|   | ||||
| @@ -3,6 +3,42 @@ const log = require('./log'); | ||||
| const eventLogService = require('./event_log'); | ||||
| 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) { | ||||
|     if (note.type === 'file') { | ||||
|         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]); | ||||
|  | ||||
|     if (!origNote || origNote.dateModified <= entity.dateModified) { | ||||
|         await sql.doInTransaction(async () => { | ||||
|         await sql.transactional(async () => { | ||||
|             await sql.replace("notes", entity); | ||||
|  | ||||
|             await syncTableService.addNoteSync(entity.noteId, sourceId); | ||||
| @@ -29,7 +65,7 @@ async function updateNote(entity, sourceId) { | ||||
| async function updateBranch(entity, sourceId) { | ||||
|     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) { | ||||
|             delete entity.isExpanded; | ||||
|  | ||||
| @@ -45,7 +81,7 @@ async function updateBranch(entity, sourceId) { | ||||
| async function updateNoteRevision(entity, sourceId) { | ||||
|     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 | ||||
|         // is the protected status (and correnspondingly title and content) which doesn't affect the dateModifiedTo | ||||
|         if (orig === null || orig.dateModifiedTo <= entity.dateModifiedTo) { | ||||
| @@ -59,7 +95,7 @@ async function updateNoteRevision(entity, sourceId) { | ||||
| } | ||||
|  | ||||
| async function updateNoteReordering(entity, sourceId) { | ||||
|     await sql.doInTransaction(async () => { | ||||
|     await sql.transactional(async () => { | ||||
|         Object.keys(entity.ordering).forEach(async key => { | ||||
|             await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity.ordering[key], key]); | ||||
|         }); | ||||
| @@ -75,7 +111,7 @@ async function updateOptions(entity, sourceId) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|     await sql.transactional(async () => { | ||||
|         if (orig === null || orig.dateModified < entity.dateModified) { | ||||
|             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]); | ||||
|  | ||||
|     if (orig === null || orig.dateAccessed < entity.dateAccessed) { | ||||
|         await sql.doInTransaction(async () => { | ||||
|         await sql.transactional(async () => { | ||||
|             await sql.replace('recent_notes', entity); | ||||
|  | ||||
|             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]); | ||||
|  | ||||
|     if (!origImage || origImage.dateModified <= entity.dateModified) { | ||||
|         await sql.doInTransaction(async () => { | ||||
|         await sql.transactional(async () => { | ||||
|             await sql.replace("images", entity); | ||||
|  | ||||
|             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]); | ||||
|  | ||||
|     if (!origNoteImage || origNoteImage.dateModified <= entity.dateModified) { | ||||
|         await sql.doInTransaction(async () => { | ||||
|         await sql.transactional(async () => { | ||||
|             await sql.replace("note_images", entity); | ||||
|  | ||||
|             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]); | ||||
|  | ||||
|     if (!origLabel || origLabel.dateModified <= entity.dateModified) { | ||||
|         await sql.doInTransaction(async () => { | ||||
|         await sql.transactional(async () => { | ||||
|             await sql.replace("labels", entity); | ||||
|  | ||||
|             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]); | ||||
|  | ||||
|     if (!apiTokenId) { | ||||
|         await sql.doInTransaction(async () => { | ||||
|         await sql.transactional(async () => { | ||||
|             await sql.replace("api_tokens", entity); | ||||
|  | ||||
|             await syncTableService.addApiTokenSync(entity.apiTokenId, sourceId); | ||||
| @@ -159,14 +195,5 @@ async function updateApiToken(entity, sourceId) { | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     updateNote, | ||||
|     updateBranch, | ||||
|     updateNoteRevision, | ||||
|     updateNoteReordering, | ||||
|     updateOptions, | ||||
|     updateRecentNotes, | ||||
|     updateImage, | ||||
|     updateNoteImage, | ||||
|     updateLabel, | ||||
|     updateApiToken | ||||
|     updateEntity | ||||
| }; | ||||
| @@ -77,7 +77,7 @@ async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) { | ||||
| } | ||||
|  | ||||
| async function sortNotesAlphabetically(parentNoteId) { | ||||
|     await sql.doInTransaction(async () => { | ||||
|     await sql.transactional(async () => { | ||||
|         const notes = await sql.getRows(`SELECT branchId, noteId, title, isProtected  | ||||
|                                        FROM notes JOIN branches USING(noteId)  | ||||
|                                        WHERE branches.isDeleted = 0 AND parentNoteId = ?`, [parentNoteId]); | ||||
|   | ||||
| @@ -132,76 +132,81 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div style="position: relative; overflow: auto; grid-area: note-content; padding-left: 10px; padding-top: 10px;" id="note-detail-wrapper"> | ||||
|         <div id="note-detail-text" class="note-detail-component"></div> | ||||
|       <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 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 style="display: flex; align-items: center;"> | ||||
|             <strong>Search string:    </strong> | ||||
|             <textarea rows="4" cols="50" id="search-string"></textarea> | ||||
|           <div id="note-detail-search" class="note-detail-component"> | ||||
|             <div style="display: flex; align-items: center;"> | ||||
|               <strong>Search string:    </strong> | ||||
|               <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<=5</code> - numerical comparison (also >, >=, <).</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> | ||||
|  | ||||
|           <br /> | ||||
|           <div id="note-detail-code" class="note-detail-component"></div> | ||||
|  | ||||
|           <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<=5</code> - numerical comparison (also >, >=, <).</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> | ||||
|           <div id="note-detail-render" class="note-detail-component"></div> | ||||
|  | ||||
|             <a href="https://github.com/zadam/trilium/wiki/Labels">Complete help on search syntax</a> | ||||
|           </p> | ||||
|           <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> | ||||
|                     | ||||
|                   <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 id="note-detail-code" class="note-detail-component"></div> | ||||
|  | ||||
|         <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> | ||||
|                   | ||||
|                 <button id="file-open" class="btn btn-primary" type="button">Open</button> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </table> | ||||
|         <div id="children-overview" style="flex-grow: 1000; flex-shrink: 1000; flex-basis: 1px; height: 100px; overflow: hidden; display: flex; flex-wrap: wrap"> | ||||
|         </div> | ||||
|  | ||||
|         <input type="file" id="file-upload" style="display: none" /> | ||||
|       </div> | ||||
|  | ||||
|       <div id="label-list"> | ||||
|   | ||||
							
								
								
									
										4
									
								
								src/www
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								src/www
									
									
									
									
									
								
							| @@ -18,8 +18,6 @@ const log = require('./services/log'); | ||||
| const appInfo = require('./services/app_info'); | ||||
| const messagingService = require('./services/messaging'); | ||||
| const utils = require('./services/utils'); | ||||
| const sql = require('./services/sql'); | ||||
| const sqlInit = require('./services/sql_init'); | ||||
|  | ||||
| const port = normalizePort(config['Network']['port'] || '3000'); | ||||
| app.set('port', port); | ||||
| @@ -56,7 +54,7 @@ httpServer.listen(port); | ||||
| httpServer.on('error', onError); | ||||
| httpServer.on('listening', onListening); | ||||
|  | ||||
| sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser)); | ||||
| messagingService.init(httpServer, sessionParser); | ||||
|  | ||||
| if (utils.isElectron()) { | ||||
|     const electronRouting = require('./routes/electron'); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|     <content url="file://$MODULE_DIR$"> | ||||
|       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> | ||||
|       <sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/dist" /> | ||||
|     </content> | ||||
|     <orderEntry type="inheritedJdk" /> | ||||
|     <orderEntry type="sourceFolder" forTests="false" /> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user